u
This commit is contained in:
65
docs/api/routes/labeltemplate/create.md
Normal file
65
docs/api/routes/labeltemplate/create.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# `POST /labeltemplate`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Create label template
|
||||||
|
|
||||||
|
Creates a new ZPL label template for the selected company.
|
||||||
|
A 50×50 1-bit PNG thumbnail is automatically rendered from the content and stored with the template.
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "…",
|
||||||
|
"width": 448,
|
||||||
|
"height": 200,
|
||||||
|
"dpi": 203,
|
||||||
|
"type": "zpl",
|
||||||
|
"content": {
|
||||||
|
"objects": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `name` | yes | string | Template name |
|
||||||
|
| `width` | yes | integer | Canvas width in pixels |
|
||||||
|
| `height` | yes | integer | Canvas height in pixels |
|
||||||
|
| `dpi` | yes | integer | Printer DPI — `203` or `300` |
|
||||||
|
| `type` | yes | string | Template type — `"zpl"` |
|
||||||
|
| `content` | yes | object | Label editor JSON content |
|
||||||
|
|
||||||
|
## Response `201`
|
||||||
|
|
||||||
|
Created label template
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"labeltemplate": {
|
||||||
|
"id": 1,
|
||||||
|
"company_id": 1,
|
||||||
|
"name": "…",
|
||||||
|
"width": 448,
|
||||||
|
"height": 200,
|
||||||
|
"dpi": 203,
|
||||||
|
"type": "zpl",
|
||||||
|
"content": {},
|
||||||
|
"thumb": "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`thumb` is a base64-encoded 50×50 1-bit PNG string, or `null` if rendering failed.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid input or missing fields — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
31
docs/api/routes/labeltemplate/delete.md
Normal file
31
docs/api/routes/labeltemplate/delete.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# `DELETE /labeltemplate/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Delete label template
|
||||||
|
|
||||||
|
Deletes a label template for the authenticated user's selected company.
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | Label template ID |
|
||||||
|
|
||||||
|
## Response `204`
|
||||||
|
|
||||||
|
Label template deleted
|
||||||
|
|
||||||
|
_No content_
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
| 404 | Label template not found — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
45
docs/api/routes/labeltemplate/get.md
Normal file
45
docs/api/routes/labeltemplate/get.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# `GET /labeltemplate/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Get label template by ID
|
||||||
|
|
||||||
|
Retrieves a single label template for the authenticated user's selected company. The `thumb` field is always included inline as a base64-encoded 50×50 1-bit PNG string (`null` if not yet rendered).
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | Label template ID |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Label template details
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"labeltemplate": {
|
||||||
|
"id": 1,
|
||||||
|
"company_id": 1,
|
||||||
|
"name": "…",
|
||||||
|
"width": 448,
|
||||||
|
"height": 200,
|
||||||
|
"dpi": 203,
|
||||||
|
"type": "zpl",
|
||||||
|
"content": {},
|
||||||
|
"thumb": "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
| 404 | Label template not found — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
40
docs/api/routes/labeltemplate/list.md
Normal file
40
docs/api/routes/labeltemplate/list.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# `GET /labeltemplate`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
List label templates
|
||||||
|
|
||||||
|
Lists all ZPL label templates for the selected company. The `thumb` field is always included inline as a base64-encoded 50×50 1-bit PNG string (`null` if not yet rendered).
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
List of label templates
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"labeltemplates": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"company_id": 1,
|
||||||
|
"name": "…",
|
||||||
|
"width": 448,
|
||||||
|
"height": 200,
|
||||||
|
"dpi": 203,
|
||||||
|
"type": "zpl",
|
||||||
|
"content": {},
|
||||||
|
"thumb": "…"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | No company selected — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
74
docs/api/routes/labeltemplate/update.md
Normal file
74
docs/api/routes/labeltemplate/update.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# `PATCH /labeltemplate/{id}`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Update label template (partial)
|
||||||
|
|
||||||
|
Updates specified fields of an existing label template for the selected company.
|
||||||
|
When `content`, `width`, or `height` is provided, the thumbnail is automatically re-rendered.
|
||||||
|
|
||||||
|
Also available as `PUT /labeltemplate/{id}`.
|
||||||
|
|
||||||
|
## Path parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `id` | yes | integer | Label template ID |
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
All fields are optional.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "…",
|
||||||
|
"width": 448,
|
||||||
|
"height": 200,
|
||||||
|
"dpi": 203,
|
||||||
|
"type": "zpl",
|
||||||
|
"content": {
|
||||||
|
"objects": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `name` | string | Template name |
|
||||||
|
| `width` | integer | Canvas width in pixels |
|
||||||
|
| `height` | integer | Canvas height in pixels |
|
||||||
|
| `dpi` | integer | Printer DPI — `203` or `300` |
|
||||||
|
| `type` | string | Template type — `"zpl"` |
|
||||||
|
| `content` | object | Label editor JSON content |
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Updated label template
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"labeltemplate": {
|
||||||
|
"id": 1,
|
||||||
|
"company_id": 1,
|
||||||
|
"name": "…",
|
||||||
|
"width": 448,
|
||||||
|
"height": 200,
|
||||||
|
"dpi": 203,
|
||||||
|
"type": "zpl",
|
||||||
|
"content": {},
|
||||||
|
"thumb": "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid input — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
| 404 | Label template not found — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
755
package-lock.json
generated
755
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,9 +30,15 @@
|
|||||||
"libllmorbiter": "file:../llmorbiter",
|
"libllmorbiter": "file:../llmorbiter",
|
||||||
"multer": "^2.4.0",
|
"multer": "^2.4.0",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
|
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
|
||||||
"sharp": "^0.35.4"
|
"sharp": "^0.35.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"swagger-jsdoc": "^6.3.0"
|
"swagger-jsdoc": "^6.3.0"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true,
|
||||||
|
"canvas@3.2.3": true,
|
||||||
|
"better-sqlite3@13.0.3": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
467
src/api/routes/labeltemplate.js
Normal file
467
src/api/routes/labeltemplate.js
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import * as db from '../../db/labeltemplate.js';
|
||||||
|
import { renderThumb } from '../../services/labelThumb.js';
|
||||||
|
import { emit } from '../sse.js';
|
||||||
|
|
||||||
|
function requireSelectedCompany(req) {
|
||||||
|
const companyId = req.user?.selectedCompanyId;
|
||||||
|
if (companyId == null) {
|
||||||
|
return { error: 'No company selected' };
|
||||||
|
}
|
||||||
|
return { companyId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInt(value, field) {
|
||||||
|
const id = Number(value);
|
||||||
|
if (!Number.isInteger(id) || id <= 0) {
|
||||||
|
return { error: `${field} is required` };
|
||||||
|
}
|
||||||
|
return { id };
|
||||||
|
}
|
||||||
|
|
||||||
|
function labeltemplateErrorStatus(error) {
|
||||||
|
if (error.message === 'No company selected') return 400;
|
||||||
|
if (error.message === 'Not authorized') return 403;
|
||||||
|
if (error.message === 'Company not found') return 404;
|
||||||
|
if (error.message === 'Label template not found') return 404;
|
||||||
|
if (error.message === 'Name required') return 400;
|
||||||
|
if (error.message === 'Invalid width') return 400;
|
||||||
|
if (error.message === 'Invalid height') return 400;
|
||||||
|
if (error.message?.startsWith('Invalid dpi')) return 400;
|
||||||
|
if (error.message?.startsWith('Invalid type')) return 400;
|
||||||
|
if (error.message === 'Content required') return 400;
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers label template routes for the authenticated user's selected company.
|
||||||
|
* @param {import('express').Application} app
|
||||||
|
* @param {string} [basePath='']
|
||||||
|
*/
|
||||||
|
export function register(app, basePath = '') {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /labeltemplate:
|
||||||
|
* 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.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: List of label templates
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* labeltemplates:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplate'
|
||||||
|
* 400:
|
||||||
|
* description: No company selected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/labeltemplate`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templates = await db.list(req.user.id);
|
||||||
|
res.status(200).json({ labeltemplates: templates.map(serializeTemplate) });
|
||||||
|
} catch (error) {
|
||||||
|
const status = labeltemplateErrorStatus(error);
|
||||||
|
if (status === 500) console.error('List label templates failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /labeltemplate/{id}:
|
||||||
|
* get:
|
||||||
|
* tags: [LabelTemplate]
|
||||||
|
* summary: Get label template by ID
|
||||||
|
* description: Retrieves a single label template for the authenticated user's selected company.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: Label template ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Label template details
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* labeltemplate:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplate'
|
||||||
|
* 400:
|
||||||
|
* description: Invalid ID or no company selected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Label template not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/labeltemplate/:id`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||||
|
if (parsedId.error) {
|
||||||
|
return res.status(400).json({ error: parsedId.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const template = await db.getById(req.user.id, parsedId.id);
|
||||||
|
if (!template) {
|
||||||
|
return res.status(404).json({ error: 'Label template not found' });
|
||||||
|
}
|
||||||
|
res.status(200).json({ labeltemplate: serializeTemplate(template) });
|
||||||
|
} catch (error) {
|
||||||
|
const status = labeltemplateErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Get label template failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /labeltemplate:
|
||||||
|
* post:
|
||||||
|
* tags: [LabelTemplate]
|
||||||
|
* summary: Create label template
|
||||||
|
* description: |
|
||||||
|
* Creates a new ZPL label template for the selected company.
|
||||||
|
* A 50×50 1-bit PNG thumbnail is automatically rendered from the content
|
||||||
|
* and stored with the template.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplateCreateInput'
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: Created label template
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* labeltemplate:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplate'
|
||||||
|
* 400:
|
||||||
|
* description: Invalid input or missing fields
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
app.post(`${basePath}/labeltemplate`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, width, height, dpi, type, content } = req.body ?? {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const thumb = await buildThumb({ width, height, content });
|
||||||
|
const template = await db.create(req.user.id, {
|
||||||
|
name,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
dpi,
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
thumb,
|
||||||
|
});
|
||||||
|
emit('labeltemplate', { companyId: selected.companyId });
|
||||||
|
res.status(201).json({ labeltemplate: serializeTemplate(template) });
|
||||||
|
} catch (error) {
|
||||||
|
const status = labeltemplateErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Create label template failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleUpdate(req, res) {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||||
|
if (parsedId.error) {
|
||||||
|
return res.status(400).json({ error: parsedId.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, width, height, dpi, type, content } = req.body ?? {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Re-render thumb whenever content/dimensions are provided
|
||||||
|
const thumb = await buildThumb({ width, height, content });
|
||||||
|
const template = await db.update(req.user.id, parsedId.id, {
|
||||||
|
name,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
dpi,
|
||||||
|
type,
|
||||||
|
content,
|
||||||
|
...(thumb !== null ? { thumb } : {}),
|
||||||
|
});
|
||||||
|
emit('labeltemplate', { companyId: selected.companyId });
|
||||||
|
res.status(200).json({ labeltemplate: serializeTemplate(template) });
|
||||||
|
} catch (error) {
|
||||||
|
const status = labeltemplateErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Update label template failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /labeltemplate/{id}:
|
||||||
|
* patch:
|
||||||
|
* tags: [LabelTemplate]
|
||||||
|
* summary: Update label template (partial)
|
||||||
|
* description: |
|
||||||
|
* Updates specified fields of an existing label template.
|
||||||
|
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: Label template ID
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplateUpdateInput'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Updated label template
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* labeltemplate:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplate'
|
||||||
|
* 400:
|
||||||
|
* description: Invalid input
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Label template not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* put:
|
||||||
|
* tags: [LabelTemplate]
|
||||||
|
* summary: Update label template
|
||||||
|
* description: |
|
||||||
|
* Replaces an existing label template.
|
||||||
|
* When `content` (or `width`/`height`) is provided, the thumbnail is automatically re-rendered.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: Label template ID
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplateUpdateInput'
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Updated label template
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* labeltemplate:
|
||||||
|
* $ref: '#/components/schemas/LabelTemplate'
|
||||||
|
* 400:
|
||||||
|
* description: Invalid input
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Label template not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
app.patch(`${basePath}/labeltemplate/:id`, handleUpdate);
|
||||||
|
app.put(`${basePath}/labeltemplate/:id`, handleUpdate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /labeltemplate/{id}:
|
||||||
|
* delete:
|
||||||
|
* tags: [LabelTemplate]
|
||||||
|
* summary: Delete label template
|
||||||
|
* description: Deletes a label template for the authenticated user's selected company.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: Label template ID
|
||||||
|
* responses:
|
||||||
|
* 204:
|
||||||
|
* description: Label template deleted
|
||||||
|
* 400:
|
||||||
|
* description: Invalid ID or no company selected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Label template not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
app.delete(`${basePath}/labeltemplate/:id`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedId = parsePositiveInt(req.params.id, 'id');
|
||||||
|
if (parsedId.error) {
|
||||||
|
return res.status(400).json({ error: parsedId.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.remove(req.user.id, parsedId.id);
|
||||||
|
emit('labeltemplate', { companyId: selected.companyId });
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
const status = labeltemplateErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Delete label template failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes a label template row for the API response.
|
||||||
|
* Converts the thumb Buffer to a base64 string (or null).
|
||||||
|
*/
|
||||||
|
function serializeTemplate(template) {
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
thumb: template.thumb
|
||||||
|
? template.thumb.toString('base64')
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to render a thumbnail from content + dimensions.
|
||||||
|
* Returns null silently if content or dimensions are missing/invalid —
|
||||||
|
* the store will reject the request with a proper error anyway.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Buffer|null>}
|
||||||
|
*/
|
||||||
|
async function buildThumb({ width, height, content }) {
|
||||||
|
try {
|
||||||
|
if (content == null) return null;
|
||||||
|
const w = Number(width);
|
||||||
|
const h = Number(height);
|
||||||
|
if (!Number.isInteger(w) || w <= 0 || !Number.isInteger(h) || h <= 0) return null;
|
||||||
|
|
||||||
|
const objects = Array.isArray(content?.objects)
|
||||||
|
? content.objects
|
||||||
|
: Array.isArray(content)
|
||||||
|
? content
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return await renderThumb({ width: w, height: h, objects });
|
||||||
|
} catch {
|
||||||
|
// Thumbnail rendering failure is non-fatal
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/db/labeltemplate.js
Normal file
26
src/db/labeltemplate.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { getStore } from './store.js';
|
||||||
|
|
||||||
|
export async function list(googleAuthId) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.labeltemplate.list(googleAuthId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getById(googleAuthId, id) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.labeltemplate.getById(googleAuthId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(googleAuthId, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.labeltemplate.create(googleAuthId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function update(googleAuthId, id, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.labeltemplate.update(googleAuthId, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(googleAuthId, id) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.labeltemplate.delete(googleAuthId, id);
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ const STORE_NAMESPACES = [
|
|||||||
'ledger',
|
'ledger',
|
||||||
'warehouse',
|
'warehouse',
|
||||||
'printer',
|
'printer',
|
||||||
|
'labeltemplate',
|
||||||
'stock',
|
'stock',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
44
src/services/labelThumb.js
Normal file
44
src/services/labelThumb.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
import { renderLabel } from 'react-canvas-label-editor';
|
||||||
|
|
||||||
|
const THUMB_SIZE = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a 50×50 1-bit indexed PNG thumbnail for a label template.
|
||||||
|
*
|
||||||
|
* Workflow:
|
||||||
|
* 1. Render the label at its native pixel resolution via renderLabel().
|
||||||
|
* 2. Resize to 50×50 using Lanczos3 (covers).
|
||||||
|
* 3. Convert to 1-bit PNG:
|
||||||
|
* - Greyscale → threshold at 128 → 1-bit PNG.
|
||||||
|
* - Pixels outside the label area (transparent in the RGBA source) are
|
||||||
|
* rendered WHITE (255) so they read as 0 in the final 1-bit output,
|
||||||
|
* keeping the file as small as possible.
|
||||||
|
*
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {number} opts.width - Label width in pixels.
|
||||||
|
* @param {number} opts.height - Label height in pixels.
|
||||||
|
* @param {Array} opts.objects - Label editor objects array.
|
||||||
|
* @returns {Promise<Buffer>} PNG buffer.
|
||||||
|
*/
|
||||||
|
export async function renderThumb({ width, height, objects }) {
|
||||||
|
// 1. Render at native resolution → RGBA PNG buffer
|
||||||
|
const nativePng = await renderLabel({ width, height, objects });
|
||||||
|
|
||||||
|
// 2. Resize to 50×50, convert to greyscale, flatten transparency to white,
|
||||||
|
// then threshold to 1-bit and output as PNG.
|
||||||
|
const thumb = await sharp(nativePng)
|
||||||
|
.resize(THUMB_SIZE, THUMB_SIZE, {
|
||||||
|
fit: 'contain',
|
||||||
|
background: { r: 255, g: 255, b: 255, alpha: 1 },
|
||||||
|
kernel: sharp.kernel.lanczos3,
|
||||||
|
})
|
||||||
|
.greyscale()
|
||||||
|
// Threshold: pixels ≥ 128 → white (0 in 1-bit), < 128 → black (1 in 1-bit)
|
||||||
|
.threshold(128)
|
||||||
|
// PNG with 1-bit colour depth; compression level 9 for smallest file
|
||||||
|
.png({ colours: 2, compressionLevel: 9, adaptiveFiltering: false })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
return thumb;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user