Compare commits

...

32 Commits

Author SHA1 Message Date
seb
2a45b43f50 u 2026-11-04 06:42:57 +01:00
seb
9f61c7dee2 u 2026-11-04 06:30:45 +01:00
seb
980578e1e4 u 2026-11-04 03:30:25 +01:00
seb
ce4dc15aac u 2026-11-04 03:22:20 +01:00
seb
da785da2dd u 2026-11-03 06:20:06 +01:00
seb
24563964a6 u 2026-11-03 04:13:13 +01:00
seb
44e91a6973 u 2026-11-03 03:54:49 +01:00
seb
de2957acf1 u 2026-11-02 00:51:15 +01:00
seb
17df88ced5 u 2026-11-02 00:19:19 +01:00
seb
d3209d73c1 u 2026-11-01 21:24:28 +01:00
seb
518106ede2 u 2026-11-01 21:16:07 +01:00
seb
833cee5040 u 2026-11-01 21:11:38 +01:00
seb
aeb79c1acb u 2026-11-01 06:20:44 +01:00
seb
ff185a59dd u 2026-11-01 06:16:29 +01:00
seb
689f12ba69 u 2026-11-01 05:57:35 +01:00
seb
bf081f875d u 2026-11-01 05:38:45 +01:00
seb
8f9c941699 u 2026-11-01 05:37:15 +01:00
seb
e6c15dd053 u 2026-11-01 05:01:21 +01:00
seb
8a5a307c50 u 2026-11-01 04:58:46 +01:00
seb
43d7049a7b u 2026-11-01 04:50:07 +01:00
seb
c60b899c4d u 2026-11-01 04:33:45 +01:00
seb
e8a5322775 u 2026-11-01 04:33:01 +01:00
seb
cb30352b57 u 2026-11-01 03:33:14 +01:00
seb
5d6b31de15 u 2026-11-01 00:38:04 +01:00
seb
8827d6fc14 u 2026-11-01 00:29:29 +01:00
seb
16f70603bc u 2026-11-01 00:24:43 +01:00
seb
ce7f5aad2a u 2026-11-01 00:20:26 +01:00
seb
26adfc5aa9 u 2026-10-31 22:14:05 +01:00
seb
0480a5dcbe u 2026-10-31 21:46:36 +01:00
seb
dd5c9646b3 u 2026-10-31 21:02:53 +01:00
seb
5a0b603306 u 2026-10-31 20:37:27 +01:00
seb
72d05a34d0 u 2026-10-31 20:28:37 +01:00
551 changed files with 15920 additions and 1503 deletions

View File

@@ -17,6 +17,7 @@ CORS_ORIGINS=http://localhost:5173
# Auth — Google OAuth client ID and JWT signing secret # Auth — Google OAuth client ID and JWT signing secret
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
JWT_SECRET=fallback_secret_please_change_in_production JWT_SECRET=fallback_secret_please_change_in_production
# Auto-create user on first login (when false, only allowlisted or invited emails can sign in / create an account)
IMPLICIT_CREATE=false IMPLICIT_CREATE=false
# Database Provider — 'sqlite' or 'mssql' (defaults to 'mssql', or 'sqlite' if SQLITE_PATH is set) # Database Provider — 'sqlite' or 'mssql' (defaults to 'mssql', or 'sqlite' if SQLITE_PATH is set)

View File

@@ -1,18 +1,16 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM debian:12-slim AS syslibs FROM debian:12-slim AS syslibs
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
libatomic1 libuuid1 libfontconfig1 libfribidi0 \ libatomic1 libuuid1 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
FROM gcr.io/distroless/cc-debian12 FROM gcr.io/distroless/cc-debian12
WORKDIR /app WORKDIR /app
# Shared dynamic libraries needed by Node.js SEA and native addons (canvas, better-sqlite3) # Shared dynamic libraries needed by Node.js SEA and native addons (better-sqlite3)
COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libatomic.so* /usr/lib/x86_64-linux-gnu/ COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libatomic.so* /usr/lib/x86_64-linux-gnu/
COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libuuid.so* /usr/lib/x86_64-linux-gnu/ COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libuuid.so* /usr/lib/x86_64-linux-gnu/
COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libfontconfig.so* /usr/lib/x86_64-linux-gnu/
COPY --from=syslibs /usr/lib/x86_64-linux-gnu/libfribidi.so* /usr/lib/x86_64-linux-gnu/
# Set production defaults: port 4208 # Set production defaults: port 4208
ENV RUNNING_IN_DOCKER=true \ ENV RUNNING_IN_DOCKER=true \

View File

@@ -41,6 +41,11 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/agent/pair.get.md`](routes/agent/pair.get.md) | `GET /agent/pair` | | [`routes/agent/pair.get.md`](routes/agent/pair.get.md) | `GET /agent/pair` |
| [`routes/agent/pair.post.md`](routes/agent/pair.post.md) | `POST /agent/pair` | | [`routes/agent/pair.post.md`](routes/agent/pair.post.md) | `POST /agent/pair` |
| [`routes/agent/state.md`](routes/agent/state.md) | `POST /agent/state` | | [`routes/agent/state.md`](routes/agent/state.md) | `POST /agent/state` |
| [`routes/agentprinter.md`](routes/agentprinter.md) | `GET /agentprinter` |
| [`routes/agentprinter/order.md`](routes/agentprinter/order.md) | `PUT /agentprinter/order` |
| [`routes/agentprinter/_id.delete.md`](routes/agentprinter/_id.delete.md) | `DELETE /agentprinter/{id}` |
| [`routes/agentprinter/_id.get.md`](routes/agentprinter/_id.get.md) | `GET /agentprinter/{id}` |
| [`routes/agentprinter/_id.patch.md`](routes/agentprinter/_id.patch.md) | `PATCH /agentprinter/{id}` |
| [`routes/article/create.md`](routes/article/create.md) | `POST /article` | | [`routes/article/create.md`](routes/article/create.md) | `POST /article` |
| [`routes/article/export.zip.md`](routes/article/export.zip.md) | `GET /article/export.zip` | | [`routes/article/export.zip.md`](routes/article/export.zip.md) | `GET /article/export.zip` |
| [`routes/article/history-list.md`](routes/article/history-list.md) | `GET /article/history` | | [`routes/article/history-list.md`](routes/article/history-list.md) | `GET /article/history` |
@@ -93,6 +98,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` | | [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` |
| [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` | | [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` |
| [`routes/auth/selected-company.md`](routes/auth/selected-company.md) | `PUT /auth/selected-company` | | [`routes/auth/selected-company.md`](routes/auth/selected-company.md) | `PUT /auth/selected-company` |
| [`routes/autoinvite/list.md`](routes/autoinvite/list.md) | `GET /autoinvite` |
| [`routes/autoinvite/create.md`](routes/autoinvite/create.md) | `POST /autoinvite` |
| [`routes/autoinvite/delete.md`](routes/autoinvite/delete.md) | `DELETE /autoinvite/{id}` |
| [`routes/backup.md`](routes/backup.md) | `GET /backup` | | [`routes/backup.md`](routes/backup.md) | `GET /backup` |
| [`routes/backup/_id/vfs.md`](routes/backup/_id/vfs.md) | `GET /backup/{id}/vfs` | | [`routes/backup/_id/vfs.md`](routes/backup/_id/vfs.md) | `GET /backup/{id}/vfs` |
| [`routes/backup/_id/vfs/recover.md`](routes/backup/_id/vfs/recover.md) | `POST /backup/{id}/vfs/recover` | | [`routes/backup/_id/vfs/recover.md`](routes/backup/_id/vfs/recover.md) | `POST /backup/{id}/vfs/recover` |
@@ -135,6 +143,10 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/companysetting/set.md`](routes/companysetting/set.md) | `PUT /company/setting/{key}` | | [`routes/companysetting/set.md`](routes/companysetting/set.md) | `PUT /company/setting/{key}` |
| [`routes/company/delete.md`](routes/company/delete.md) | `DELETE /company/{id}` | | [`routes/company/delete.md`](routes/company/delete.md) | `DELETE /company/{id}` |
| [`routes/company/update.md`](routes/company/update.md) | `PUT /company/{id}` | | [`routes/company/update.md`](routes/company/update.md) | `PUT /company/{id}` |
| [`routes/companyuser/list.md`](routes/companyuser/list.md) | `GET /companyuser` |
| [`routes/companyuser/create.md`](routes/companyuser/create.md) | `POST /companyuser` |
| [`routes/companyuser/delete.md`](routes/companyuser/delete.md) | `DELETE /companyuser/{id}` |
| [`routes/companyuser/update.md`](routes/companyuser/update.md) | `PATCH /companyuser/{id}` |
| [`routes/customergroup/list.md`](routes/customergroup/list.md) | `GET /customergroup` | | [`routes/customergroup/list.md`](routes/customergroup/list.md) | `GET /customergroup` |
| [`routes/customergroup/create.md`](routes/customergroup/create.md) | `POST /customergroup` | | [`routes/customergroup/create.md`](routes/customergroup/create.md) | `POST /customergroup` |
| [`routes/customergroup/order.md`](routes/customergroup/order.md) | `PUT /customergroup/order` | | [`routes/customergroup/order.md`](routes/customergroup/order.md) | `PUT /customergroup/order` |
@@ -168,6 +180,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` | | [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` |
| [`routes/labeltemplate/list.md`](routes/labeltemplate/list.md) | `GET /labeltemplate` | | [`routes/labeltemplate/list.md`](routes/labeltemplate/list.md) | `GET /labeltemplate` |
| [`routes/labeltemplate/create.md`](routes/labeltemplate/create.md) | `POST /labeltemplate` | | [`routes/labeltemplate/create.md`](routes/labeltemplate/create.md) | `POST /labeltemplate` |
| [`routes/labeltemplate/testprint.md`](routes/labeltemplate/testprint.md) | `POST /labeltemplate/testprint` |
| [`routes/labeltemplate/delete.md`](routes/labeltemplate/delete.md) | `DELETE /labeltemplate/{id}` | | [`routes/labeltemplate/delete.md`](routes/labeltemplate/delete.md) | `DELETE /labeltemplate/{id}` |
| [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) | `GET /labeltemplate/{id}` | | [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) | `GET /labeltemplate/{id}` |
| [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PATCH /labeltemplate/{id}` | | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PATCH /labeltemplate/{id}` |
@@ -223,14 +236,22 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/printer/list.md`](routes/printer/list.md) | `GET /printer` | | [`routes/printer/list.md`](routes/printer/list.md) | `GET /printer` |
| [`routes/printer/create.md`](routes/printer/create.md) | `POST /printer` | | [`routes/printer/create.md`](routes/printer/create.md) | `POST /printer` |
| [`routes/printer/order.md`](routes/printer/order.md) | `PUT /printer/order` | | [`routes/printer/order.md`](routes/printer/order.md) | `PUT /printer/order` |
| [`routes/printer/state.md`](routes/printer/state.md) | `GET /printer/state` |
| [`routes/printer/delete.md`](routes/printer/delete.md) | `DELETE /printer/{id}` | | [`routes/printer/delete.md`](routes/printer/delete.md) | `DELETE /printer/{id}` |
| [`routes/printer/get.md`](routes/printer/get.md) | `GET /printer/{id}` | | [`routes/printer/get.md`](routes/printer/get.md) | `GET /printer/{id}` |
| [`routes/printer/update.md`](routes/printer/update.md) | `PATCH /printer/{id}` | | [`routes/printer/update.md`](routes/printer/update.md) | `PATCH /printer/{id}` |
| [`routes/printer/_id.put.md`](routes/printer/_id.put.md) | `PUT /printer/{id}` | | [`routes/printer/_id.put.md`](routes/printer/_id.put.md) | `PUT /printer/{id}` |
| [`routes/printer/_id/state.md`](routes/printer/_id/state.md) | `GET /printer/{id}/state` |
| [`routes/printeragent.get.md`](routes/printeragent.get.md) | `GET /printeragent` | | [`routes/printeragent.get.md`](routes/printeragent.get.md) | `GET /printeragent` |
| [`routes/printeragent.put.md`](routes/printeragent.put.md) | `PUT /printeragent` | | [`routes/printeragent.put.md`](routes/printeragent.put.md) | `PUT /printeragent` |
| [`routes/printeragent/_id.delete.md`](routes/printeragent/_id.delete.md) | `DELETE /printeragent/{id}` | | [`routes/printeragent/_id.delete.md`](routes/printeragent/_id.delete.md) | `DELETE /printeragent/{id}` |
| [`routes/printeragent/_id.patch.md`](routes/printeragent/_id.patch.md) | `PATCH /printeragent/{id}` | | [`routes/printeragent/_id.patch.md`](routes/printeragent/_id.patch.md) | `PATCH /printeragent/{id}` |
| [`routes/printerjob.get.md`](routes/printerjob.get.md) | `GET /printerjob` |
| [`routes/printerjob.post.md`](routes/printerjob.post.md) | `POST /printerjob` |
| [`routes/printerjob/cancel-all.md`](routes/printerjob/cancel-all.md) | `POST /printerjob/cancel-all` |
| [`routes/printerjob/_id.delete.md`](routes/printerjob/_id.delete.md) | `DELETE /printerjob/{id}` |
| [`routes/printerjob/_id.get.md`](routes/printerjob/_id.get.md) | `GET /printerjob/{id}` |
| [`routes/printerjob/_id/cancel.md`](routes/printerjob/_id/cancel.md) | `POST /printerjob/{id}/cancel` |
| [`routes/propertyname.get.md`](routes/propertyname.get.md) | `GET /propertyname` | | [`routes/propertyname.get.md`](routes/propertyname.get.md) | `GET /propertyname` |
| [`routes/propertyname.post.md`](routes/propertyname.post.md) | `POST /propertyname` | | [`routes/propertyname.post.md`](routes/propertyname.post.md) | `POST /propertyname` |
| [`routes/propertyname/order.md`](routes/propertyname/order.md) | `PUT /propertyname/order` | | [`routes/propertyname/order.md`](routes/propertyname/order.md) | `PUT /propertyname/order` |
@@ -324,6 +345,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/warehouse/get.md`](routes/warehouse/get.md) | `GET /warehouse/{id}` | | [`routes/warehouse/get.md`](routes/warehouse/get.md) | `GET /warehouse/{id}` |
| [`routes/warehouse/update.md`](routes/warehouse/update.md) | `PATCH /warehouse/{id}` | | [`routes/warehouse/update.md`](routes/warehouse/update.md) | `PATCH /warehouse/{id}` |
| [`routes/warehouse/_id.put.md`](routes/warehouse/_id.put.md) | `PUT /warehouse/{id}` | | [`routes/warehouse/_id.put.md`](routes/warehouse/_id.put.md) | `PUT /warehouse/{id}` |
| [`routes/zpljob.md`](routes/zpljob.md) | `POST /zpljob` |
Route overview: [`routes.md`](routes.md) | All routes: [`routes-all.md`](routes-all.md). Route overview: [`routes.md`](routes.md) | All routes: [`routes-all.md`](routes-all.md).
@@ -333,13 +355,16 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A
| Signal | Emitted by | | Signal | Emitted by |
|--------|------------| |--------|------------|
| `agentprinter` | `PUT /agentprinter/order`, `DELETE /agentprinter/{id}`, `PATCH /agentprinter/{id}` |
| `article-<sku>` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `POST /article/{sku}/supplier`, `PUT /article/{sku}/supplier/order`, `DELETE /article/{sku}/supplier/{id}`, `PATCH /article/{sku}/supplier/{id}`, `PUT /article/{sku}/track_stock`, `PUT /article/{sku}/weight`, `POST /stock/adjust` | | `article-<sku>` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `POST /article/{sku}/supplier`, `PUT /article/{sku}/supplier/order`, `DELETE /article/{sku}/supplier/{id}`, `PATCH /article/{sku}/supplier/{id}`, `PUT /article/{sku}/track_stock`, `PUT /article/{sku}/weight`, `POST /stock/adjust` |
| `articles` | `POST /article`, `DELETE /article/{sku}`, `PATCH /article/{sku}`, `DELETE /category/{id}`, `DELETE /shipment/{id}`, `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` | | `articles` | `POST /article`, `DELETE /article/{sku}`, `PATCH /article/{sku}`, `DELETE /category/{id}`, `DELETE /shipment/{id}`, `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` |
| `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` | | `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` |
| `autoinvite` | `POST /autoinvite`, `DELETE /autoinvite/{id}` |
| `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` | | `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` |
| `carrier` | `POST /carrier`, `PUT /carrier/order`, `DELETE /carrier/{id}`, `PATCH /carrier/{id}` | | `carrier` | `POST /carrier`, `PUT /carrier/order`, `DELETE /carrier/{id}`, `PATCH /carrier/{id}` |
| `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image`, `POST /category/{id}/image/generate` | | `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image`, `POST /category/{id}/image/generate` |
| `company` | `PUT /auth/selected-company`, `POST /company`, `DELETE /company/{id}`, `PUT /company/{id}` | | `company` | `PUT /auth/selected-company`, `POST /company`, `DELETE /company/{id}`, `PUT /company/{id}` |
| `companyuser` | `POST /companyuser`, `DELETE /companyuser/{id}`, `PATCH /companyuser/{id}` |
| `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf`, `POST /stock/accept` | | `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf`, `POST /stock/accept` |
| `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` | | `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` |
| `ledger` | `DELETE /shipment/{id}`, `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` | | `ledger` | `DELETE /shipment/{id}`, `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` |

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 292 API endpoints across all modules. Overview of all 314 API endpoints across all modules.
### Notation ### Notation
@@ -9,45 +9,48 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T19[], externalNumber?: str|null, createdAt?: str, shipping?: T21|null, shippings?: T21[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T11[] }` - `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T23|null, shippings?: T23[], payment?: T33|null, payments?: T33[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T10[] }`
- `T2` = `{ id: int, number: str, type: str, date: str, lines: T19[], shippings: T21[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T11[], state?: str }` - `T2` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T23[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T10[], state?: str }`
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T11[] }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T10[] }`
- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T22[], files: T11[], externalNumber?: str|null, createdAt?: str, ledger?: T37 }` - `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T24[], files: T10[], externalNumber?: str|null, createdAt?: str, ledger?: T40 }`
- `T5` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T6|null }` - `T5` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T6|null }`
- `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }` - `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T11, totalSizeBytes: int, durationMs: int }`
- `T7` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }` - `T7` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
- `T8` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, clientserial?: str|null, clientmodel?: str|null, clientstate?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str, sort?: int }` - `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T42|null, incoming?: { date: str, amount: num }[] }`
- `T9` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T39|null, incoming?: { date: str, amount: num }[] }` - `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] }, thumb?: str|null }`
- `T10` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] }, thumb?: str|null }` - `T10` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T11` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }` - `T11` = `{ vfs: T41, storage: T41, database: T35 }`
- `T12` = `{ vfs: T38, storage: T38, database: T32 }` - `T12` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str, sort?: int, enabled?: bool }`
- `T13` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - `T13` = `{ id: int, printeragentId: int, type: str, enabled: bool, sort: int, model?: str|null, serial?: str|null, open?: bool, pause?: bool, error?: bool, online?: bool, labelLength?: int|null }`
- `T14` = `{ id: int, company_id: int, prompt: str, max_itr: int, cron: str, created_at: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, lastrun_at?: str|null }` - `T14` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T15` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }` - `T15` = `{ id: int, company_id: int, prompt: str, max_itr: int, cron: str, created_at: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, lastrun_at?: str|null }`
- `T16` = `{ id: int, name: str, sort: int, exclusive: bool, values: T28[], articleCount?: int }` - `T16` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T17` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }` - `T17` = `{ id: int, name: str, sort: int, exclusive: bool, values: T30[], articleCount?: int }`
- `T18` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }` - `T18` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }`
- `T19` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }` - `T19` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num, role: str }`
- `T20` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }` - `T20` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }`
- `T21` = `{ id: int, manifestId: int, number: str, carrierId: int, type: str, state: str, trackingCode?: str|null, carrier?: str|null }` - `T21` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }`
- `T22` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }` - `T22` = `{ id: int, agentprinterId: int, type: str, state: str, data: str, createdAt: str, printeragentId?: int|null, completedAt?: str|null }`
- `T23` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T23` = `{ id: int, manifestId: int, number: str, carrierId: int, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T24` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }` - `T24` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T25` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }` - `T25` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T26` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }` - `T26` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T27` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }` - `T27` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T28` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }` - `T28` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T29` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }` - `T29` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T30` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }` - `T30` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T31` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }` - `T31` = `{ id: int, googleAuthId: int, email: str, role: str, picture?: str|null }`
- `T32` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }` - `T32` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T33` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }` - `T33` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T34` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T34` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }`
- `T35` = `{ id: int, entityId: int, contactType: str, contact: str }` - `T35` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
- `T36` = `{ id: int, name: str, createdAt: str, updatedAt: str }` - `T36` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T37` = `{ totalPositions: int, completelyOnLedgerPositions: int }` - `T37` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T38` = `{ name: str, sizeBytes: int, sourceExists: bool }` - `T38` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T39` = `{ net: num, taxId: int, taxRate?: num|null }` - `T39` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T40` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T41` = `{ name: str, sizeBytes: int, sourceExists: bool }`
- `T42` = `{ net: num, taxId: int, taxRate?: num|null }`
### Routes ### Routes
@@ -86,7 +89,25 @@ _none_
- Returns: `200 { id: int, name: str, companyId: int, token: str, pairedAt: str, clientserial: str|null, clientmodel: str|null, agentversion: str|null, sort: int }` - Returns: `200 { id: int, name: str, companyId: int, token: str, pairedAt: str, clientserial: str|null, clientmodel: str|null, agentversion: str|null, sort: int }`
#### Post agent state — `POST /agent/state` #### Post agent state — `POST /agent/state`
- Params: `body: { state?: any }` - Params: `body: { state?: any }`
- Returns: `200 { success: bool, agent: T8 }` - Returns: `200 { success: bool, agent: T12 }`
### Section: /agentprinter
#### List agent printers — `GET /agentprinter`
- Params: `query: printeragentId?: int`
- Returns: `200 { agentprinters: T13[] }`
#### Reorder agent printers — `PUT /agentprinter/order`
- Params: `body: { agentprinterIds: int[], printeragentId?: int }`
- Returns: `200 { agentprinterIds: int[] }`
#### Delete agent printer — `DELETE /agentprinter/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get agent printer — `GET /agentprinter/{id}`
- Params: `id: int`
- Returns: `200 { agentprinter: T13 }`
#### Update agent printer — `PATCH /agentprinter/{id}`
- Params: `id: int, body: { enabled?: bool, sort?: int }`
- Returns: `200 { agentprinter: T13 }`
### Section: /article ### Section: /article
@@ -104,7 +125,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get article by SKU — `GET /article/{sku}` #### Get article by SKU — `GET /article/{sku}`
- Params: `sku: str` - Params: `sku: str`
- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T9[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }` - Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T29, brand: T28|null, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T8[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }`
#### Update article by SKU — `PATCH /article/{sku}` #### Update article by SKU — `PATCH /article/{sku}`
- Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` - Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }`
- Returns: `204 void` - Returns: `204 void`
@@ -162,11 +183,11 @@ _none_
#### Toggle parent mode for an article — `POST /article/{sku}/parent` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `204 void` - Returns: `204 void`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### Replace POS prices by SKU (net > gross) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, taxRate?: num, minmultiplier?: num }[] }`
- Returns: `204 void` - Returns: `204 void`
#### Print article label — `POST /article/{sku}/print` #### Print article label — `POST /article/{sku}/print`
- Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }` - Params: `sku: str, body: { templateId: int, agentprinterId: int, quantity: int }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
#### List article properties — `GET /article/{sku}/property` #### List article properties — `GET /article/{sku}/property`
- Params: `sku: str` - Params: `sku: str`
@@ -179,10 +200,10 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List article suppliers — `GET /article/{sku}/supplier` #### List article suppliers — `GET /article/{sku}/supplier`
- Params: `sku: str` - Params: `sku: str`
- Returns: `200 { suppliers: T9[] }` - Returns: `200 { suppliers: T8[] }`
#### Assign supplier to article — `POST /article/{sku}/supplier` #### Assign supplier to article — `POST /article/{sku}/supplier`
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }` - Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `201 { supplier: T9 }` - Returns: `201 { supplier: T8 }`
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order` #### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
- Params: `sku: str, body: { ids: int[] }` - Params: `sku: str, body: { ids: int[] }`
- Returns: `200 { ids: int[] }` - Returns: `200 { ids: int[] }`
@@ -191,7 +212,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}` #### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }` - Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `200 { supplier: T9 }` - Returns: `200 { supplier: T8 }`
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock` #### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
- Params: `sku: str, body: { track_stock: bool }` - Params: `sku: str, body: { track_stock: bool }`
- Returns: `204 void` - Returns: `204 void`
@@ -203,19 +224,19 @@ _none_
#### List attribute names — `GET /attributename` #### List attribute names — `GET /attributename`
_none_ _none_
- Returns: `200 { attributenames: T16[] }` - Returns: `200 { attributenames: T17[] }`
#### Create attribute name — `POST /attributename` #### Create attribute name — `POST /attributename`
- Params: `body: { name: str, sort?: int, exclusive?: bool }` - Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { attributename: T16 }` - Returns: `201 { attributename: T17 }`
#### Reorder attribute names — `PUT /attributename/order` #### Reorder attribute names — `PUT /attributename/order`
- Params: `body: { attributenameIds: int[] }` - Params: `body: { attributenameIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List values for an attribute — `GET /attributename/{attributeId}/value` #### List values for an attribute — `GET /attributename/{attributeId}/value`
- Params: `attributeId: int` - Params: `attributeId: int`
- Returns: `200 { values: T28[] }` - Returns: `200 { values: T30[] }`
#### Create value for an attribute — `POST /attributename/{attributeId}/value` #### Create value for an attribute — `POST /attributename/{attributeId}/value`
- Params: `attributeId: int, body: { value: str, sort?: int }` - Params: `attributeId: int, body: { value: str, sort?: int }`
- Returns: `201 { attributevalue: T28 }` - Returns: `201 { attributevalue: T30 }`
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order` #### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
- Params: `attributeId: int, body: { attributevalueIds: int[] }` - Params: `attributeId: int, body: { attributevalueIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -224,7 +245,7 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update attribute name — `PATCH /attributename/{id}` #### Update attribute name — `PATCH /attributename/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { attributename: T16 }` - Returns: `200 { attributename: T17 }`
### Section: /attributevalue ### Section: /attributevalue
@@ -233,28 +254,40 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update attribute value — `PATCH /attributevalue/{id}` #### Update attribute value — `PATCH /attributevalue/{id}`
- Params: `id: int, body: { value?: str, sort?: int }` - Params: `id: int, body: { value?: str, sort?: int }`
- Returns: `200 { attributevalue: T28 }` - Returns: `200 { attributevalue: T30 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image` #### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T34[] } }` - Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T37[] } }`
#### Add or replace attribute value image — `POST /attributevalue/{id}/image` #### Add or replace attribute value image — `POST /attributevalue/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T34[] } }` - Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T37[] } }`
### Section: /auth ### Section: /auth
#### Login with Google ID token — `POST /auth/login` #### Login with Google ID token — `POST /auth/login`
- Params: `body: { idToken: str }` - Params: `body: { idToken: str }`
- Returns: `200 { message: str, user: T33 }` - Returns: `200 { message: str, user: T36 }`
#### Logout — `POST /auth/logout` #### Logout — `POST /auth/logout`
_none_ _none_
- Returns: `200 { message: str }` - Returns: `200 { message: str }`
#### Current user — `GET /auth/me` #### Current user — `GET /auth/me`
_none_ _none_
- Returns: `200 { user: T33, googleClientId: str, schemaVersion: int, vhost: bool, vhostSettings: str }` - Returns: `200 { user: T36, googleClientId: str, schemaVersion: int, vhost: bool, vhostSettings: str }`
#### Set selected company — `PUT /auth/selected-company` #### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }` - Params: `body: { companyId: int }`
- Returns: `200 { user: T33 }` - Returns: `200 { user: T36 }`
### Section: /autoinvite
#### List auto-invitations — `GET /autoinvite`
_none_
- Returns: `200 { autoinvites: { id: int, email: str, role: str, createdAt: str }[] }`
#### Create auto-invitation — `POST /autoinvite`
- Params: `body: { email: str, role?: str }`
- Returns: `201 { autoinvite: { id: int, email: str, role: str } }`
#### Delete auto-invitation — `DELETE /autoinvite/{id}`
- Params: `id: int`
- Returns: `204 void`
### Section: /backup ### Section: /backup
@@ -275,7 +308,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }` - Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand` #### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }` - Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T34[], gsprpinfo?: str|null } }` - Returns: `201 { brand: { id: int, name: str, pics: T37[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}` #### Delete brand — `DELETE /brand/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
@@ -284,19 +317,19 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }` - Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image` #### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T34[] } }` - Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T37[] } }`
#### Add or replace brand image — `POST /brand/{id}/image` #### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T34[] } }` - Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T37[] } }`
### Section: /carrier ### Section: /carrier
#### List carriers — `GET /carrier` #### List carriers — `GET /carrier`
_none_ _none_
- Returns: `200 { carriers: T31[] }` - Returns: `200 { carriers: T34[] }`
#### Create carrier — `POST /carrier` #### Create carrier — `POST /carrier`
- Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }` - Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
- Returns: `201 { carrier: T31 }` - Returns: `201 { carrier: T34 }`
#### Reorder carriers — `PUT /carrier/order` #### Reorder carriers — `PUT /carrier/order`
- Params: `body: { carrierIds: int[] }` - Params: `body: { carrierIds: int[] }`
- Returns: `200 { carrierIds: int[] }` - Returns: `200 { carrierIds: int[] }`
@@ -305,10 +338,10 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get carrier by ID — `GET /carrier/{id}` #### Get carrier by ID — `GET /carrier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { carrier: T31 }` - Returns: `200 { carrier: T34 }`
#### Update carrier — `PATCH /carrier/{id}` #### Update carrier — `PATCH /carrier/{id}`
- Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }` - Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
- Returns: `200 { carrier: T31 }` - Returns: `200 { carrier: T34 }`
### Section: /category ### Section: /category
@@ -332,7 +365,7 @@ _none_
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }` - Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
#### Articles in category — `GET /category/{id}/articles` #### Articles in category — `GET /category/{id}/articles`
- Params: `id: int, query: includeChildren?: bool` - Params: `id: int, query: includeChildren?: bool`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null, brand?: T26|null, articlewarehousesummary?: { id: int, articleId: int, warehouseId: int, stock: num, article_id?: int, warehouse_id?: int, warehouseName?: str|null }[], warehouses?: { id: int, name: str, stock: num }[] }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null, brand?: T28|null, articlewarehousesummary?: { id: int, articleId: int, warehouseId: int, stock: num, article_id?: int, warehouse_id?: int, warehouseName?: str|null }[], warehouses?: { id: int, name: str, stock: num }[] }[] }`
#### List category attribute values — `GET /category/{id}/attributevalue` #### List category attribute values — `GET /category/{id}/attributevalue`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }` - Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
@@ -362,10 +395,10 @@ _none_
#### List companies — `GET /company` #### List companies — `GET /company`
_none_ _none_
- Returns: `200 { companies: T18[] }` - Returns: `200 { companies: T19[] }`
#### Create company — `POST /company` #### Create company — `POST /company`
- Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }` - Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `201 { company: T18, user: T33 }` - Returns: `201 { company: T19, user: T36 }`
#### Export company catalog as zip archive (SSE) — `GET /company/export` #### Export company catalog as zip archive (SSE) — `GET /company/export`
- Params: `query: stream?: str` - Params: `query: stream?: str`
- Returns: `200 SSE` - Returns: `200 SSE`
@@ -395,7 +428,22 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update company — `PUT /company/{id}` #### Update company — `PUT /company/{id}`
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }` - Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `200 { company: T18 }` - Returns: `200 { company: T19 }`
### Section: /companyuser
#### List company users — `GET /companyuser`
_none_
- Returns: `200 { companyusers: T31[] }`
#### Add an existing user to the company — `POST /companyuser`
- Params: `body: { email: str, role?: str }`
- Returns: `201 { companyuser: T31 }`
#### Remove a user from the company — `DELETE /companyuser/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Change a company user's role — `PATCH /companyuser/{id}`
- Params: `id: int, body: { role: str }`
- Returns: `200 { companyuser: T31 }`
### Section: /customergroup ### Section: /customergroup
@@ -419,7 +467,7 @@ _none_
#### List deliveries — `GET /delivery` #### List deliveries — `GET /delivery`
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T37 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T40 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create delivery — `POST /delivery` #### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, supplierId: int, lines: { amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `body: { externalNumber: str, date: str, supplierId: int, lines: { amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 { delivery: T4 }` - Returns: `201 { delivery: T4 }`
@@ -440,52 +488,52 @@ _none_
- Returns: `200 str` - Returns: `200 str`
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf` #### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T11, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
### Section: /entity ### Section: /entity
#### List entities — `GET /entity` #### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str` - Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T25[] }` - Returns: `200 { entities: T27[] }`
#### Create entity — `POST /entity` #### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }` - Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 { entity: T25 }` - Returns: `201 { entity: T27 }`
#### Peek next customer number — `GET /entity/next-number` #### Peek next customer number — `GET /entity/next-number`
_none_ _none_
- Returns: `200 { number: str }` - Returns: `200 { number: str }`
#### List entity addresses — `GET /entity/{entityId}/address` #### List entity addresses — `GET /entity/{entityId}/address`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { addresses: T13[] }` - Returns: `200 { addresses: T14[] }`
#### Create entity address — `POST /entity/{entityId}/address` #### Create entity address — `POST /entity/{entityId}/address`
- Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `201 { address: T13 }` - Returns: `201 { address: T14 }`
#### Delete entity address — `DELETE /entity/{entityId}/address/{id}` #### Delete entity address — `DELETE /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity address — `PATCH /entity/{entityId}/address/{id}` #### Update entity address — `PATCH /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `200 { address: T13 }` - Returns: `200 { address: T14 }`
#### List entity contacts — `GET /entity/{entityId}/contact` #### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { contacts: T35[] }` - Returns: `200 { contacts: T38[] }`
#### Create entity contact — `POST /entity/{entityId}/contact` #### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }` - Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T35 }` - Returns: `201 { contact: T38 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T35 }` - Returns: `200 { contact: T38 }`
#### Delete entity — `DELETE /entity/{id}` #### Delete entity — `DELETE /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get entity — `GET /entity/{id}` #### Get entity — `GET /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { entity: T25 }` - Returns: `200 { entity: T27 }`
#### Update entity — `PATCH /entity/{id}` #### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }` - Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 { entity: T25 }` - Returns: `200 { entity: T27 }`
### Section: /events ### Section: /events
@@ -503,8 +551,13 @@ _none_
- `ledger`: `{}` - `ledger`: `{}`
- `attributename`: `{}` - `attributename`: `{}`
- `propertyname`: `{}` - `propertyname`: `{}`
- `companyuser`: `{}`
- `autoinvite`: `{}`
- `labeltemplate`: `{}` - `labeltemplate`: `{}`
- `printer`: `{}` - `printer`: `{}`
- `printeragent`: `{}`
- `agentprinter`: `{ printers } or {}`
- `printerjob`: `{ job } or { jobId, state }`
- `warehouse`: `{}` - `warehouse`: `{}`
- `supplier`: `{}` - `supplier`: `{}`
- `vfs`: `{}` - `vfs`: `{}`
@@ -533,35 +586,38 @@ _none_
_none_ _none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }` - Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate` #### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T23[] } }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T25[] } }`
- Returns: `201 { labeltemplate: T10 }` - Returns: `201 { labeltemplate: T9 }`
#### Test print label template — `POST /labeltemplate/testprint`
- Params: `body: { agentprinterId: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }`
- Returns: `200 { success: bool }`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get label template by ID — `GET /labeltemplate/{id}` #### Get label template by ID — `GET /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { labeltemplate: T10 }` - Returns: `200 { labeltemplate: T9 }`
#### Update label template (partial) — `PATCH /labeltemplate/{id}` #### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T23[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T25[] } }`
- Returns: `200 { labeltemplate: T10 }` - Returns: `200 { labeltemplate: T9 }`
#### Update label template — `PUT /labeltemplate/{id}` #### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T23[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T25[] } }`
- Returns: `200 { labeltemplate: T10 }` - Returns: `200 { labeltemplate: T9 }`
### Section: /ledger ### Section: /ledger
#### Query ledger by manifest article ID or SKU — `GET /ledger` #### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 { ledger: T15[] }` - Returns: `200 { ledger: T16[] }`
#### Query company-wide ledger history — `GET /ledger/history` #### Query company-wide ledger history — `GET /ledger/history`
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { ledger: { id: int, date: str, transaction_type: str, transaction_text: str, sku?: str|null, name?: str|null, amount?: num }[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { ledger: { id: int, date: str, transaction_type: str, transaction_text: str, sku?: str|null, name?: str|null, amount?: num }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}` #### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str` - Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 { ledger: T15[] }` - Returns: `200 { ledger: T16[] }`
#### Query ledger by article SKU — `GET /ledger/sku/{sku}` #### Query ledger by article SKU — `GET /ledger/sku/{sku}`
- Params: `sku: str, query: transaction_type?: str` - Params: `sku: str, query: transaction_type?: str`
- Returns: `200 { ledger: T15[] }` - Returns: `200 { ledger: T16[] }`
### Section: /mcp ### Section: /mcp
@@ -591,16 +647,16 @@ _none_
#### List conversations — `GET /modelconversation` #### List conversations — `GET /modelconversation`
_none_ _none_
- Returns: `200 { conversations: T17[] }` - Returns: `200 { conversations: T18[] }`
#### Create conversation — `POST /modelconversation` #### Create conversation — `POST /modelconversation`
- Params: `body: { provider: str, model: str }` - Params: `body: { provider: str, model: str }`
- Returns: `201 { conversation: T17 }` - Returns: `201 { conversation: T18 }`
#### Delete conversation — `DELETE /modelconversation/{id}` #### Delete conversation — `DELETE /modelconversation/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get conversation — `GET /modelconversation/{id}` #### Get conversation — `GET /modelconversation/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { conversation: T17 }` - Returns: `200 { conversation: T18 }`
#### List conversation messages — `GET /modelconversation/{id}/message` #### List conversation messages — `GET /modelconversation/{id}/message`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { messages: { id: int, conversation_id: int, created_at: str, role: str, message: str }[] }` - Returns: `200 { messages: { id: int, conversation_id: int, created_at: str, role: str, message: str }[] }`
@@ -641,7 +697,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order` #### Create order — `POST /order`
- Params: `body: { number: str, date: str, entityId: int, lines: T20[], externalNumber?: str|null }` - Params: `body: { number: str, date: str, entityId: int, lines: T21[], externalNumber?: str|null }`
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Delete order — `DELETE /order/{id}` #### Delete order — `DELETE /order/{id}`
- Params: `id: int` - Params: `id: int`
@@ -650,7 +706,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { order: T1 }` - Returns: `200 { order: T1 }`
#### Update order — `PATCH /order/{id}` #### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T20[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T21[] }`
- Returns: `200 { order: T1 }` - Returns: `200 { order: T1 }`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}` #### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -660,13 +716,13 @@ _none_
- Returns: `200 str` - Returns: `200 str`
#### List payments of an order — `GET /order/{id}/payment` #### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { payments: T30[] }` - Returns: `200 { payments: T33[] }`
#### Add payment to an order — `POST /order/{id}/payment` #### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }` - Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: T30 }` - Returns: `201 { payment: T33 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf` #### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T11, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
#### List shipments of an order — `GET /order/{id}/shipment` #### List shipments of an order — `GET /order/{id}/shipment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shipments: T2[] }` - Returns: `200 { shipments: T2[] }`
@@ -675,10 +731,10 @@ _none_
- Returns: `201 { shipment: T2 }` - Returns: `201 { shipment: T2 }`
#### List shippings of an order — `GET /order/{id}/shipping` #### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T21[] }` - Returns: `200 { shippings: T23[] }`
#### Create shipping for an order — `POST /order/{id}/shipping` #### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }` - Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }`
- Returns: `201 { shipping: T21 }` - Returns: `201 { shipping: T23 }`
### Section: /package ### Section: /package
@@ -723,49 +779,76 @@ _none_
#### List printers — `GET /printer` #### List printers — `GET /printer`
_none_ _none_
- Returns: `200 { printers: T24[] }` - Returns: `200 { printers: T26[] }`
#### Create printer — `POST /printer` #### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 { printer: T24 }` - Returns: `201 { printer: T26 }`
#### Reorder printers — `PUT /printer/order` #### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }` - Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }` - Returns: `200 { printerIds: int[] }`
#### Get live printer states — `GET /printer/state`
- Params: `query: serial?: str, agentId?: int`
- Returns: `200 { printers: {}[] }`
#### Delete printer — `DELETE /printer/{id}` #### Delete printer — `DELETE /printer/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}` #### Get printer by ID — `GET /printer/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { printer: T24 }` - Returns: `200 { printer: T26 }`
#### Update printer (partial) — `PATCH /printer/{id}` #### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 { printer: T24 }` - Returns: `200 { printer: T26 }`
#### Update printer — `PUT /printer/{id}` #### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 { printer: T24 }` - Returns: `200 { printer: T26 }`
#### Get live state for a printer — `GET /printer/{id}/state`
- Params: `id: str`
- Returns: `200 { printer: {} }`
### Section: /printeragent ### Section: /printeragent
#### List printer agents — `GET /printeragent` #### List printer agents — `GET /printeragent`
_none_ _none_
- Returns: `200 { printeragents: T8[] }` - Returns: `200 { printeragents: T12[] }`
#### Create printer agent — `PUT /printeragent` #### Create printer agent — `PUT /printeragent`
- Params: `body: { name: str, sort?: int }` - Params: `body: { name: str, sort?: int, enabled?: bool }`
- Returns: `200 { printeragent: T8 }` - Returns: `200 { printeragent: T12 }`
#### Delete printer agent — `DELETE /printeragent/{id}` #### Delete printer agent — `DELETE /printeragent/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update printer agent — `PATCH /printeragent/{id}` #### Update printer agent — `PATCH /printeragent/{id}`
- Params: `id: int, body: { name?: str, sort?: int }` - Params: `id: int, body: { name?: str, sort?: int, enabled?: bool }`
- Returns: `200 { printeragent: T8 }` - Returns: `200 { printeragent: T12 }`
### Section: /printerjob
#### List printer jobs — `GET /printerjob`
- Params: `query: agentprinterId?: int, state?: str`
- Returns: `200 { printerjobs: T22[] }`
#### Create print job — `POST /printerjob`
- Params: `body: { agentprinterId: int, data: str, type?: str }`
- Returns: `201 { printerjob: T22 }`
#### Cancel all pending and processing printer jobs — `POST /printerjob/cancel-all`
- Params: `body?: { agentprinterId?: int }`
- Returns: `200 { success: bool, cancelledCount: int, jobs: T22[] }`
#### Cancel and delete printer job — `DELETE /printerjob/{id}`
- Params: `id: int`
- Returns: `200 { success: bool, deletedJob: T22 }`
#### Get printer job — `GET /printerjob/{id}`
- Params: `id: int`
- Returns: `200 { printerjob: T22 }`
#### Cancel printer job — `POST /printerjob/{id}/cancel`
- Params: `id: int`
- Returns: `200 { success: bool, printerjob: T22 }`
### Section: /propertyname ### Section: /propertyname
#### List property names — `GET /propertyname` #### List property names — `GET /propertyname`
_none_ _none_
- Returns: `200 { propertynames: T29[] }` - Returns: `200 { propertynames: T32[] }`
#### Create property name — `POST /propertyname` #### Create property name — `POST /propertyname`
- Params: `body: { name: str, sort?: int, exclusive?: bool }` - Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { propertyname: T29 }` - Returns: `201 { propertyname: T32 }`
#### Reorder property names — `PUT /propertyname/order` #### Reorder property names — `PUT /propertyname/order`
- Params: `body: { propertynameIds: int[] }` - Params: `body: { propertynameIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -774,7 +857,7 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update property name — `PATCH /propertyname/{id}` #### Update property name — `PATCH /propertyname/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { propertyname: T29 }` - Returns: `200 { propertyname: T32 }`
### Section: /public ### Section: /public
@@ -786,7 +869,7 @@ _none_
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }`
#### Get public article detail — `GET /public/{companyId}/articles/{sku}` #### Get public article detail — `GET /public/{companyId}/articles/{sku}`
- Params: `companyId: int, sku: str` - Params: `companyId: int, sku: str`
- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num|null, weight: num|null, trackStock: bool, unit: T27|null, brand: T26|null, price: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }, prices: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }[], categories: { id: int, name: str, slug: str }[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], isParent: bool, parentSku: str|null, variants: { id: int, sku: str, sort: int, name: str, slug: str, attributeValueId: int|null, attributeValue: str|null, unitamount: num|null, weight: num|null, trackStock: bool }[] } }` - Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num|null, weight: num|null, trackStock: bool, unit: T29|null, brand: T28|null, price: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }, prices: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }[], categories: { id: int, name: str, slug: str }[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], isParent: bool, parentSku: str|null, variants: { id: int, sku: str, sort: int, name: str, slug: str, attributeValueId: int|null, attributeValue: str|null, unitamount: num|null, weight: num|null, trackStock: bool }[] } }`
#### List public categories — `GET /public/{companyId}/categories` #### List public categories — `GET /public/{companyId}/categories`
- Params: `companyId: int` - Params: `companyId: int`
- Returns: `200 { categories: { id: int, parentId: int|null, sort: int, name: str, slug: str, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` - Returns: `200 { categories: { id: int, parentId: int|null, sort: int, name: str, slug: str, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }`
@@ -797,7 +880,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T3[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { quotes: T3[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote` #### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T21[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 { quote: T3 }` - Returns: `201 { quote: T3 }`
#### Delete quote — `DELETE /quote/{id}` #### Delete quote — `DELETE /quote/{id}`
- Params: `id: int` - Params: `id: int`
@@ -806,7 +889,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { quote: T3 }` - Returns: `200 { quote: T3 }`
#### Update quote — `PATCH /quote/{id}` #### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T20[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T21[] }`
- Returns: `200 { quote: T3 }` - Returns: `200 { quote: T3 }`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` #### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -815,11 +898,11 @@ _none_
- Params: `id: int, fileId: int, query: download?: bool` - Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str` - Returns: `200 str`
#### Create order from quote — `POST /quote/{id}/order` #### Create order from quote — `POST /quote/{id}/order`
- Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T20[] }` - Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T21[] }`
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf` #### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T11, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
### Section: /retoure ### Section: /retoure
@@ -906,13 +989,13 @@ _none_
- Returns: `200 str` - Returns: `200 str`
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf` #### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T11, fileUrl: str }` - Returns: `201 { attachment: T10, fileUrl: str }`
#### List packages of a shipment — `GET /shipment/{id}/shipping` #### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T21[] }` - Returns: `200 { shippings: T23[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping` #### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }` - Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }`
- Returns: `201 { shipping: T21 }` - Returns: `201 { shipping: T23 }`
### Section: /shipping ### Section: /shipping
@@ -921,7 +1004,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}` #### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrierId?: int, warehouseId?: int }` - Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrierId?: int, warehouseId?: int }`
- Returns: `200 { shipping: T21 }` - Returns: `200 { shipping: T23 }`
#### List packages of a shipping — `GET /shipping/{id}/package` #### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` - Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -942,7 +1025,7 @@ _none_
- Returns: `200` - Returns: `200`
#### Adjust stock manually — `POST /stock/adjust` #### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T15[] }` - Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T16[] }`
#### Ship shipment manifest item(s) — `POST /stock/ship` #### Ship shipment manifest item(s) — `POST /stock/ship`
- Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }` - Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }`
- Returns: `200` - Returns: `200`
@@ -951,40 +1034,40 @@ _none_
#### List subagent schedules — `GET /subagentschedule` #### List subagent schedules — `GET /subagentschedule`
_none_ _none_
- Returns: `200 { schedules: T14[] }` - Returns: `200 { schedules: T15[] }`
#### Create subagent schedule — `POST /subagentschedule` #### Create subagent schedule — `POST /subagentschedule`
- Params: `body: { prompt: str, cron: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, max_itr?: int }` - Params: `body: { prompt: str, cron: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, max_itr?: int }`
- Returns: `201 { schedule: T14 }` - Returns: `201 { schedule: T15 }`
#### Delete subagent schedule — `DELETE /subagentschedule/{id}` #### Delete subagent schedule — `DELETE /subagentschedule/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get subagent schedule by ID — `GET /subagentschedule/{id}` #### Get subagent schedule by ID — `GET /subagentschedule/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { schedule: T14 }` - Returns: `200 { schedule: T15 }`
#### Update subagent schedule — `PUT /subagentschedule/{id}` #### Update subagent schedule — `PUT /subagentschedule/{id}`
- Params: `id: int, body: { prompt?: str, cron?: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, max_itr?: int, lastrun_at?: str|null }` - Params: `id: int, body: { prompt?: str, cron?: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, max_itr?: int, lastrun_at?: str|null }`
- Returns: `200 { schedule: T14 }` - Returns: `200 { schedule: T15 }`
### Section: /supplier ### Section: /supplier
#### List suppliers — `GET /supplier` #### List suppliers — `GET /supplier`
_none_ _none_
- Returns: `200 { suppliers: T36[] }` - Returns: `200 { suppliers: T39[] }`
#### Create supplier — `POST /supplier` #### Create supplier — `POST /supplier`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { supplier: T36 }` - Returns: `201 { supplier: T39 }`
#### Delete supplier — `DELETE /supplier/{id}` #### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}` #### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T36 }` - Returns: `200 { supplier: T39 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles` #### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: str` - Params: `id: int, query: skus?: str`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T39|null, incoming?: { date: str, amount: num }[] }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T42|null, incoming?: { date: str, amount: num }[] }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }` - Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T39|null, incoming?: { date: str, amount: num }[] }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T42|null, incoming?: { date: str, amount: num }[] }[] }`
### Section: /thumb ### Section: /thumb
@@ -999,10 +1082,10 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### List units — `GET /unit` #### List units — `GET /unit`
_none_ _none_
- Returns: `200 { units: T27[] }` - Returns: `200 { units: T29[] }`
#### Upsert unit — `PUT /unit` #### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }` - Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T27 }` - Returns: `200 { unit: T29 }`
### Section: /v1 ### Section: /v1
@@ -1093,3 +1176,9 @@ _none_
#### Update warehouse — `PUT /warehouse/{id}` #### Update warehouse — `PUT /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }` - Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 { warehouse: { id: int, name: str, sort: int } }` - Returns: `200 { warehouse: { id: int, name: str, sort: int } }`
### Section: /zpljob
#### Create ZPL print job — `POST /zpljob`
- Params: `body: { agentprinterId: int, data: str, copiescount?: int }`
- Returns: `201 { printerjob: T22 }`

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview # Dash API Routes Overview
Overview of all 212 API endpoints across all modules. Overview of all 196 API endpoints across all modules.
### Notation ### Notation
@@ -9,54 +9,37 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T13[], externalNumber?: str|null, createdAt?: str, shipping?: T15|null, shippings?: T15[], payment?: T23|null, payments?: T23[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T8[] }` - `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T11[], externalNumber?: str|null, createdAt?: str, shipping?: T13|null, shippings?: T13[], payment?: T21|null, payments?: T21[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T7[] }`
- `T2` = `{ id: int, number: str, type: str, date: str, lines: T13[], shippings: T15[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T8[], state?: str }` - `T2` = `{ id: int, number: str, type: str, date: str, lines: T11[], shippings: T13[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T7[], state?: str }`
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T13[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T8[] }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T11[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T7[] }`
- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T16[], files: T8[], externalNumber?: str|null, createdAt?: str, ledger?: T28 }` - `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T14[], files: T7[], externalNumber?: str|null, createdAt?: str, ledger?: T26 }`
- `T5` = `{ id: int, name: str, companyId?: int, token?: str|null, pairingpin?: str|null, pairedAt?: str|null, clientserial?: str|null, clientmodel?: str|null, clientstate?: str|null, agentversion?: str|null, lastseenAt?: str|null, updatedAt?: str, sort?: int }` - `T5` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T27|null, incoming?: { date: str, amount: num }[] }`
- `T6` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T29|null, incoming?: { date: str, amount: num }[] }` - `T6` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T15[] }, thumb?: str|null }`
- `T7` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T17[] }, thumb?: str|null }` - `T7` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T8` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }` - `T8` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T9` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - `T9` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T10` = `{ id: int, company_id: int, prompt: str, max_itr: int, cron: str, created_at: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, lastrun_at?: str|null }` - `T10` = `{ id: int, name: str, sort: int, exclusive: bool, values: T18[], articleCount?: int }`
- `T11` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }` - `T11` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }`
- `T12` = `{ id: int, name: str, sort: int, exclusive: bool, values: T21[], articleCount?: int }` - `T12` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }`
- `T13` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null, shippable: bool }` - `T13` = `{ id: int, manifestId: int, number: str, carrierId: int, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T14` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null, shippable?: bool }` - `T14` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T15` = `{ id: int, manifestId: int, number: str, carrierId: int, type: str, state: str, trackingCode?: str|null, carrier?: str|null }` - `T15` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T16` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }` - `T16` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T17` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T17` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T18` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }` - `T18` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T19` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }` - `T19` = `{ id: int, googleAuthId: int, email: str, role: str, picture?: str|null }`
- `T20` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }` - `T20` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T21` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }` - `T21` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T22` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }` - `T22` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }`
- `T23` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }` - `T23` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T24` = `{ id: int, name: str, sort: int, hasPackages: bool, isReverse: bool }` - `T24` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T25` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T25` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T26` = `{ id: int, entityId: int, contactType: str, contact: str }` - `T26` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T27` = `{ id: int, name: str, createdAt: str, updatedAt: str }` - `T27` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T28` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T29` = `{ net: num, taxId: int, taxRate?: num|null }`
### Routes ### Routes
### Section: /agent
#### Printer agent SSE stream — `GET /agent/events`
- Params: `query: token?: str, agentversion?: str`
- Returns: `200 SSE`
#### Pair printer agent via query parameters — `GET /agent/pair`
- Params: `query: authCode?: str, pairingpin?: str, serial?: str, model?: str, version?: str`
- Returns: `200`
#### Pair printer agent — `POST /agent/pair`
- Params: `body: { pairingpin: str, clientserial?: str, clientmodel?: str, agentversion?: str }`
- Returns: `200 { id: int, name: str, companyId: int, token: str, pairedAt: str, clientserial: str|null, clientmodel: str|null, agentversion: str|null, sort: int }`
#### Post agent state — `POST /agent/state`
- Params: `body: { state?: any }`
- Returns: `200 { success: bool, agent: T5 }`
### Section: /article ### Section: /article
#### Create article in category — `POST /article` #### Create article in category — `POST /article`
@@ -70,7 +53,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `204 void` - Returns: `204 void`
#### Get article by SKU — `GET /article/{sku}` #### Get article by SKU — `GET /article/{sku}`
- Params: `sku: str` - Params: `sku: str`
- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T20, brand: { name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T6[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }` - Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T17, brand: { name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T5[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }`
#### Update article by SKU — `PATCH /article/{sku}` #### Update article by SKU — `PATCH /article/{sku}`
- Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` - Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }`
- Returns: `204 void` - Returns: `204 void`
@@ -125,11 +108,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
#### Toggle parent mode for an article — `POST /article/{sku}/parent` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `204 void` - Returns: `204 void`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### Replace POS prices by SKU (net > gross) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, taxRate?: num, minmultiplier?: num }[] }`
- Returns: `204 void` - Returns: `204 void`
#### Print article label — `POST /article/{sku}/print` #### Print article label — `POST /article/{sku}/print`
- Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }` - Params: `sku: str, body: { templateId: int, agentprinterId: int, quantity: int }`
- Returns: `200 { success: bool }` - Returns: `200 { success: bool }`
#### List article properties — `GET /article/{sku}/property` #### List article properties — `GET /article/{sku}/property`
- Params: `sku: str` - Params: `sku: str`
@@ -142,10 +125,10 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List article suppliers — `GET /article/{sku}/supplier` #### List article suppliers — `GET /article/{sku}/supplier`
- Params: `sku: str` - Params: `sku: str`
- Returns: `200 { suppliers: T6[] }` - Returns: `200 { suppliers: T5[] }`
#### Assign supplier to article — `POST /article/{sku}/supplier` #### Assign supplier to article — `POST /article/{sku}/supplier`
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }` - Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `201 { supplier: T6 }` - Returns: `201 { supplier: T5 }`
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order` #### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
- Params: `sku: str, body: { ids: int[] }` - Params: `sku: str, body: { ids: int[] }`
- Returns: `200 { ids: int[] }` - Returns: `200 { ids: int[] }`
@@ -154,7 +137,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `204 void` - Returns: `204 void`
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}` #### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }` - Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `200 { supplier: T6 }` - Returns: `200 { supplier: T5 }`
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock` #### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
- Params: `sku: str, body: { track_stock: bool }` - Params: `sku: str, body: { track_stock: bool }`
- Returns: `204 void` - Returns: `204 void`
@@ -166,19 +149,19 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
#### List attribute names — `GET /attributename` #### List attribute names — `GET /attributename`
_none_ _none_
- Returns: `200 { attributenames: T12[] }` - Returns: `200 { attributenames: T10[] }`
#### Create attribute name — `POST /attributename` #### Create attribute name — `POST /attributename`
- Params: `body: { name: str, sort?: int, exclusive?: bool }` - Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { attributename: T12 }` - Returns: `201 { attributename: T10 }`
#### Reorder attribute names — `PUT /attributename/order` #### Reorder attribute names — `PUT /attributename/order`
- Params: `body: { attributenameIds: int[] }` - Params: `body: { attributenameIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List values for an attribute — `GET /attributename/{attributeId}/value` #### List values for an attribute — `GET /attributename/{attributeId}/value`
- Params: `attributeId: int` - Params: `attributeId: int`
- Returns: `200 { values: T21[] }` - Returns: `200 { values: T18[] }`
#### Create value for an attribute — `POST /attributename/{attributeId}/value` #### Create value for an attribute — `POST /attributename/{attributeId}/value`
- Params: `attributeId: int, body: { value: str, sort?: int }` - Params: `attributeId: int, body: { value: str, sort?: int }`
- Returns: `201 { attributevalue: T21 }` - Returns: `201 { attributevalue: T18 }`
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order` #### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
- Params: `attributeId: int, body: { attributevalueIds: int[] }` - Params: `attributeId: int, body: { attributevalueIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -187,7 +170,7 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update attribute name — `PATCH /attributename/{id}` #### Update attribute name — `PATCH /attributename/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { attributename: T12 }` - Returns: `200 { attributename: T10 }`
### Section: /attributevalue ### Section: /attributevalue
@@ -196,10 +179,22 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update attribute value — `PATCH /attributevalue/{id}` #### Update attribute value — `PATCH /attributevalue/{id}`
- Params: `id: int, body: { value?: str, sort?: int }` - Params: `id: int, body: { value?: str, sort?: int }`
- Returns: `200 { attributevalue: T21 }` - Returns: `200 { attributevalue: T18 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image` #### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T25[] } }` - Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T23[] } }`
### Section: /autoinvite
#### List auto-invitations — `GET /autoinvite`
_none_
- Returns: `200 { autoinvites: { id: int, email: str, role: str, createdAt: str }[] }`
#### Create auto-invitation — `POST /autoinvite`
- Params: `body: { email: str, role?: str }`
- Returns: `201 { autoinvite: { id: int, email: str, role: str } }`
#### Delete auto-invitation — `DELETE /autoinvite/{id}`
- Params: `id: int`
- Returns: `204 void`
### Section: /brand ### Section: /brand
@@ -208,7 +203,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }` - Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand` #### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }` - Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T25[], gsprpinfo?: str|null } }` - Returns: `201 { brand: { id: int, name: str, pics: T23[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}` #### Delete brand — `DELETE /brand/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
@@ -217,16 +212,16 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }` - Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image` #### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T25[] } }` - Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T23[] } }`
### Section: /carrier ### Section: /carrier
#### List carriers — `GET /carrier` #### List carriers — `GET /carrier`
_none_ _none_
- Returns: `200 { carriers: T24[] }` - Returns: `200 { carriers: T22[] }`
#### Create carrier — `POST /carrier` #### Create carrier — `POST /carrier`
- Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }` - Params: `body: { name: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
- Returns: `201 { carrier: T24 }` - Returns: `201 { carrier: T22 }`
#### Reorder carriers — `PUT /carrier/order` #### Reorder carriers — `PUT /carrier/order`
- Params: `body: { carrierIds: int[] }` - Params: `body: { carrierIds: int[] }`
- Returns: `200 { carrierIds: int[] }` - Returns: `200 { carrierIds: int[] }`
@@ -235,10 +230,10 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get carrier by ID — `GET /carrier/{id}` #### Get carrier by ID — `GET /carrier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { carrier: T24 }` - Returns: `200 { carrier: T22 }`
#### Update carrier — `PATCH /carrier/{id}` #### Update carrier — `PATCH /carrier/{id}`
- Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }` - Params: `id: int, body: { name?: str, sort?: int, hasPackages?: bool, isReverse?: bool }`
- Returns: `200 { carrier: T24 }` - Returns: `200 { carrier: T22 }`
### Section: /category ### Section: /category
@@ -286,7 +281,7 @@ _none_
#### List companies — `GET /company` #### List companies — `GET /company`
_none_ _none_
- Returns: `200 { companies: { id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }[] }` - Returns: `200 { companies: { id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num, role: str }[] }`
#### Get company history — `GET /company/history` #### Get company history — `GET /company/history`
- Params: `query: limit?: int` - Params: `query: limit?: int`
- Returns: `200 { history: { id: int, date: str, text: str, companyuserId?: int|null }[], truncated: bool }` - Returns: `200 { history: { id: int, date: str, text: str, companyuserId?: int|null }[], truncated: bool }`
@@ -307,7 +302,22 @@ _none_
- Returns: `200 { setting: { key: str, value: str|null } }` - Returns: `200 { setting: { key: str, value: str|null } }`
#### Update company — `PUT /company/{id}` #### Update company — `PUT /company/{id}`
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }` - Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `200 { company: { id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num } }` - Returns: `200 { company: { id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num, role: str } }`
### Section: /companyuser
#### List company users — `GET /companyuser`
_none_
- Returns: `200 { companyusers: T19[] }`
#### Add an existing user to the company — `POST /companyuser`
- Params: `body: { email: str, role?: str }`
- Returns: `201 { companyuser: T19 }`
#### Remove a user from the company — `DELETE /companyuser/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Change a company user's role — `PATCH /companyuser/{id}`
- Params: `id: int, body: { role: str }`
- Returns: `200 { companyuser: T19 }`
### Section: /customergroup ### Section: /customergroup
@@ -331,7 +341,7 @@ _none_
#### List deliveries — `GET /delivery` #### List deliveries — `GET /delivery`
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T28 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T26 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create delivery — `POST /delivery` #### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, supplierId: int, lines: { amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `body: { externalNumber: str, date: str, supplierId: int, lines: { amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 { delivery: T4 }` - Returns: `201 { delivery: T4 }`
@@ -349,58 +359,52 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf` #### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T8, fileUrl: str }` - Returns: `201 { attachment: T7, fileUrl: str }`
### Section: /entity ### Section: /entity
#### List entities — `GET /entity` #### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str` - Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T19[] }` - Returns: `200 { entities: T16[] }`
#### Create entity — `POST /entity` #### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }` - Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 { entity: T19 }` - Returns: `201 { entity: T16 }`
#### Peek next customer number — `GET /entity/next-number` #### Peek next customer number — `GET /entity/next-number`
_none_ _none_
- Returns: `200 { number: str }` - Returns: `200 { number: str }`
#### List entity addresses — `GET /entity/{entityId}/address` #### List entity addresses — `GET /entity/{entityId}/address`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { addresses: T9[] }` - Returns: `200 { addresses: T8[] }`
#### Create entity address — `POST /entity/{entityId}/address` #### Create entity address — `POST /entity/{entityId}/address`
- Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `201 { address: T9 }` - Returns: `201 { address: T8 }`
#### Delete entity address — `DELETE /entity/{entityId}/address/{id}` #### Delete entity address — `DELETE /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity address — `PATCH /entity/{entityId}/address/{id}` #### Update entity address — `PATCH /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `200 { address: T9 }` - Returns: `200 { address: T8 }`
#### List entity contacts — `GET /entity/{entityId}/contact` #### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { contacts: T26[] }` - Returns: `200 { contacts: T24[] }`
#### Create entity contact — `POST /entity/{entityId}/contact` #### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }` - Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T26 }` - Returns: `201 { contact: T24 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T26 }` - Returns: `200 { contact: T24 }`
#### Delete entity — `DELETE /entity/{id}` #### Delete entity — `DELETE /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get entity — `GET /entity/{id}` #### Get entity — `GET /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { entity: T19 }` - Returns: `200 { entity: T16 }`
#### Update entity — `PATCH /entity/{id}` #### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }` - Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 { entity: T19 }` - Returns: `200 { entity: T16 }`
### Section: /health
#### Health latency history — `GET /health/latency`
- Params: `query: range?: str`
- Returns: `200 { raw: { timestamp: int, value: num }[], avg: { timestamp: int, value: num }[], max: { timestamp: int, value: num }[] }`
### Section: /labeltemplate ### Section: /labeltemplate
@@ -408,35 +412,38 @@ _none_
_none_ _none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }` - Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate` #### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T17[] } }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T15[] } }`
- Returns: `201 { labeltemplate: T7 }` - Returns: `201 { labeltemplate: T6 }`
#### Test print label template — `POST /labeltemplate/testprint`
- Params: `body: { agentprinterId: int, templateId?: int, objects?: any[], width?: int, height?: int, dpi?: int, sku?: str, quantity?: int }`
- Returns: `200 { success: bool }`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get label template by ID — `GET /labeltemplate/{id}` #### Get label template by ID — `GET /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { labeltemplate: T7 }` - Returns: `200 { labeltemplate: T6 }`
#### Update label template (partial) — `PATCH /labeltemplate/{id}` #### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T17[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T15[] } }`
- Returns: `200 { labeltemplate: T7 }` - Returns: `200 { labeltemplate: T6 }`
#### Update label template — `PUT /labeltemplate/{id}` #### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T17[] } }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T15[] } }`
- Returns: `200 { labeltemplate: T7 }` - Returns: `200 { labeltemplate: T6 }`
### Section: /ledger ### Section: /ledger
#### Query ledger by manifest article ID or SKU — `GET /ledger` #### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 { ledger: T11[] }` - Returns: `200 { ledger: T9[] }`
#### Query company-wide ledger history — `GET /ledger/history` #### Query company-wide ledger history — `GET /ledger/history`
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { ledger: { id: int, date: str, transaction_type: str, transaction_text: str, sku?: str|null, name?: str|null, amount?: num }[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { ledger: { id: int, date: str, transaction_type: str, transaction_text: str, sku?: str|null, name?: str|null, amount?: num }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}` #### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str` - Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 { ledger: T11[] }` - Returns: `200 { ledger: T9[] }`
#### Query ledger by article SKU — `GET /ledger/sku/{sku}` #### Query ledger by article SKU — `GET /ledger/sku/{sku}`
- Params: `sku: str, query: transaction_type?: str` - Params: `sku: str, query: transaction_type?: str`
- Returns: `200 { ledger: T11[] }` - Returns: `200 { ledger: T9[] }`
### Section: /number ### Section: /number
@@ -453,7 +460,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order` #### Create order — `POST /order`
- Params: `body: { number: str, date: str, entityId: int, lines: T14[], externalNumber?: str|null }` - Params: `body: { number: str, date: str, entityId: int, lines: T12[], externalNumber?: str|null }`
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Delete order — `DELETE /order/{id}` #### Delete order — `DELETE /order/{id}`
- Params: `id: int` - Params: `id: int`
@@ -462,20 +469,20 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { order: T1 }` - Returns: `200 { order: T1 }`
#### Update order — `PATCH /order/{id}` #### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T14[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T12[] }`
- Returns: `200 { order: T1 }` - Returns: `200 { order: T1 }`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}` #### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
- Returns: `204 void` - Returns: `204 void`
#### List payments of an order — `GET /order/{id}/payment` #### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { payments: T23[] }` - Returns: `200 { payments: T21[] }`
#### Add payment to an order — `POST /order/{id}/payment` #### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }` - Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: T23 }` - Returns: `201 { payment: T21 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf` #### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T8, fileUrl: str }` - Returns: `201 { attachment: T7, fileUrl: str }`
#### List shipments of an order — `GET /order/{id}/shipment` #### List shipments of an order — `GET /order/{id}/shipment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shipments: T2[] }` - Returns: `200 { shipments: T2[] }`
@@ -484,10 +491,10 @@ _none_
- Returns: `201 { shipment: T2 }` - Returns: `201 { shipment: T2 }`
#### List shippings of an order — `GET /order/{id}/shipping` #### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T15[] }` - Returns: `200 { shippings: T13[] }`
#### Create shipping for an order — `POST /order/{id}/shipping` #### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }` - Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }`
- Returns: `201 { shipping: T15 }` - Returns: `201 { shipping: T13 }`
### Section: /package ### Section: /package
@@ -501,53 +508,14 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
### Section: /printer
#### List printers — `GET /printer`
_none_
- Returns: `200 { printers: T18[] }`
#### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 { printer: T18 }`
#### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }`
#### Delete printer — `DELETE /printer/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}`
- Params: `id: int`
- Returns: `200 { printer: T18 }`
#### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 { printer: T18 }`
#### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 { printer: T18 }`
### Section: /printeragent
#### List printer agents — `GET /printeragent`
_none_
- Returns: `200 { printeragents: T5[] }`
#### Create printer agent — `PUT /printeragent`
- Params: `body: { name: str, sort?: int }`
- Returns: `200 { printeragent: T5 }`
#### Delete printer agent — `DELETE /printeragent/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Update printer agent — `PATCH /printeragent/{id}`
- Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 { printeragent: T5 }`
### Section: /propertyname ### Section: /propertyname
#### List property names — `GET /propertyname` #### List property names — `GET /propertyname`
_none_ _none_
- Returns: `200 { propertynames: T22[] }` - Returns: `200 { propertynames: T20[] }`
#### Create property name — `POST /propertyname` #### Create property name — `POST /propertyname`
- Params: `body: { name: str, sort?: int, exclusive?: bool }` - Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { propertyname: T22 }` - Returns: `201 { propertyname: T20 }`
#### Reorder property names — `PUT /propertyname/order` #### Reorder property names — `PUT /propertyname/order`
- Params: `body: { propertynameIds: int[] }` - Params: `body: { propertynameIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -556,7 +524,7 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update property name — `PATCH /propertyname/{id}` #### Update property name — `PATCH /propertyname/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { propertyname: T22 }` - Returns: `200 { propertyname: T20 }`
### Section: /quote ### Section: /quote
@@ -564,7 +532,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T3[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { quotes: T3[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote` #### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T14[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T12[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 { quote: T3 }` - Returns: `201 { quote: T3 }`
#### Delete quote — `DELETE /quote/{id}` #### Delete quote — `DELETE /quote/{id}`
- Params: `id: int` - Params: `id: int`
@@ -573,17 +541,17 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 { quote: T3 }` - Returns: `200 { quote: T3 }`
#### Update quote — `PATCH /quote/{id}` #### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T14[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T12[] }`
- Returns: `200 { quote: T3 }` - Returns: `200 { quote: T3 }`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` #### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
- Returns: `204 void` - Returns: `204 void`
#### Create order from quote — `POST /quote/{id}/order` #### Create order from quote — `POST /quote/{id}/order`
- Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T14[] }` - Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T12[] }`
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf` #### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T8, fileUrl: str }` - Returns: `201 { attachment: T7, fileUrl: str }`
### Section: /retoure ### Section: /retoure
@@ -652,13 +620,13 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf` #### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 { attachment: T8, fileUrl: str }` - Returns: `201 { attachment: T7, fileUrl: str }`
#### List packages of a shipment — `GET /shipment/{id}/shipping` #### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T15[] }` - Returns: `200 { shippings: T13[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping` #### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }` - Params: `id: int, body: { carrierId: int, type: str, state: str, trackingCode?: str, warehouseId?: int }`
- Returns: `201 { shipping: T15 }` - Returns: `201 { shipping: T13 }`
### Section: /shipping ### Section: /shipping
@@ -667,7 +635,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}` #### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrierId?: int, warehouseId?: int }` - Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrierId?: int, warehouseId?: int }`
- Returns: `200 { shipping: T15 }` - Returns: `200 { shipping: T13 }`
#### List packages of a shipping — `GET /shipping/{id}/package` #### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` - Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -675,12 +643,6 @@ _none_
- Params: `id: int, body: { entityaddressId: int, type: str, state: str, details?: str }` - Params: `id: int, body: { entityaddressId: int, type: str, state: str, details?: str }`
- Returns: `201 { package: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null } }` - Returns: `201 { package: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null } }`
### Section: /smallthumb
#### Small thumb image by hash — `GET /smallthumb/{fullHash}`
- Params: `fullHash: str`
- Returns: `200 avif`
### Section: /stock ### Section: /stock
#### Accept delivery manifest item(s) — `POST /stock/accept` #### Accept delivery manifest item(s) — `POST /stock/accept`
@@ -688,49 +650,31 @@ _none_
- Returns: `200` - Returns: `200`
#### Adjust stock manually — `POST /stock/adjust` #### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T11[] }` - Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T9[] }`
#### Ship shipment manifest item(s) — `POST /stock/ship` #### Ship shipment manifest item(s) — `POST /stock/ship`
- Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }` - Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }`
- Returns: `200` - Returns: `200`
### Section: /subagentschedule
#### List subagent schedules — `GET /subagentschedule`
_none_
- Returns: `200 { schedules: T10[] }`
#### Create subagent schedule — `POST /subagentschedule`
- Params: `body: { prompt: str, cron: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, max_itr?: int }`
- Returns: `201 { schedule: T10 }`
#### Delete subagent schedule — `DELETE /subagentschedule/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get subagent schedule by ID — `GET /subagentschedule/{id}`
- Params: `id: int`
- Returns: `200 { schedule: T10 }`
#### Update subagent schedule — `PUT /subagentschedule/{id}`
- Params: `id: int, body: { prompt?: str, cron?: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, max_itr?: int, lastrun_at?: str|null }`
- Returns: `200 { schedule: T10 }`
### Section: /supplier ### Section: /supplier
#### List suppliers — `GET /supplier` #### List suppliers — `GET /supplier`
_none_ _none_
- Returns: `200 { suppliers: T27[] }` - Returns: `200 { suppliers: T25[] }`
#### Create supplier — `POST /supplier` #### Create supplier — `POST /supplier`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { supplier: T27 }` - Returns: `201 { supplier: T25 }`
#### Delete supplier — `DELETE /supplier/{id}` #### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}` #### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T27 }` - Returns: `200 { supplier: T25 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles` #### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: str` - Params: `id: int, query: skus?: str`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T29|null, incoming?: { date: str, amount: num }[] }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T27|null, incoming?: { date: str, amount: num }[] }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }` - Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T29|null, incoming?: { date: str, amount: num }[] }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T27|null, incoming?: { date: str, amount: num }[] }[] }`
### Section: /unit ### Section: /unit
@@ -739,19 +683,10 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### List units — `GET /unit` #### List units — `GET /unit`
_none_ _none_
- Returns: `200 { units: T20[] }` - Returns: `200 { units: T17[] }`
#### Upsert unit — `PUT /unit` #### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }` - Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T20 }` - Returns: `200 { unit: T17 }`
### Section: /v1
#### JTL-POS deleted entity synchronization — `GET /v1/deletedentity`
- Params: `query: lastChangedDeletedEntity?: str, limit?: int`
- Returns: `200 { entityId: str, entityType: int, lastChanged: int }[]`
#### JTL-POS order upload — `POST /v1/order`
- Params: `query: mandantId?: str, body: { orders?: {}[] }`
- Returns: `200 { status: str, externalOrderId: str, message: str }[]`
### Section: /warehouse ### Section: /warehouse

View File

@@ -33,13 +33,11 @@ State recorded
"token": "…", "token": "…",
"pairingpin": "…", "pairingpin": "…",
"pairedAt": "…", "pairedAt": "…",
"clientserial": "…",
"clientmodel": "…",
"clientstate": "…",
"agentversion": "…", "agentversion": "…",
"lastseenAt": "…", "lastseenAt": "…",
"updatedAt": "…", "updatedAt": "…",
"sort": 1 "sort": 1,
"enabled": true
} }
} }
``` ```

View File

@@ -0,0 +1,51 @@
# `GET /agentprinter`
Protected. Requires a valid session cookie (`credentials: 'include'`).
List agent printers
Lists agent printers for the authenticated user's selected company,
optionally filtered by printer agent ID.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `printeragentId` | no | integer | Optional printer agent ID filter |
## Response `200`
List of agent printers
```json
{
"agentprinters": [
{
"id": 1,
"printeragentId": 1,
"type": "…",
"model": "…",
"serial": "…",
"enabled": true,
"sort": 1,
"open": true,
"pause": true,
"error": true,
"online": true,
"labelLength": 1
}
]
}
```
## 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`._

View File

@@ -0,0 +1,35 @@
# `DELETE /agentprinter/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete agent printer
Deletes an agent printer from the selected company.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Signals
On success, emits SSE signal: `agentprinter` (see [`GET /events`](../events.md)).
## Response `204`
Agent printer deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 404 | Agent printer not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,48 @@
# `GET /agentprinter/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get agent printer
Retrieves a single agent printer by ID.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Response `200`
Agent printer details
```json
{
"agentprinter": {
"id": 1,
"printeragentId": 1,
"type": "…",
"model": "…",
"serial": "…",
"enabled": true,
"sort": 1,
"open": true,
"pause": true,
"error": true,
"online": true,
"labelLength": 1
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 404 | Agent printer not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,67 @@
# `PATCH /agentprinter/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Update agent printer
Updates enabled and/or display order (sort) of an agent printer.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `enabled` | no | boolean | |
| `sort` | no | integer | |
```json
{
"enabled": true,
"sort": 1
}
```
## Signals
On success, emits SSE signal: `agentprinter` (see [`GET /events`](../events.md)).
## Response `200`
Updated agent printer
```json
{
"agentprinter": {
"id": 1,
"printeragentId": 1,
"type": "…",
"model": "…",
"serial": "…",
"enabled": true,
"sort": 1,
"open": true,
"pause": true,
"error": true,
"online": true,
"labelLength": 1
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid input or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 404 | Agent printer not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,51 @@
# `PUT /agentprinter/order`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Reorder agent printers
Reorders agent printers for a specific printer agent within the selected company.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `printeragentId` | no | integer | Optional agent id. If omitted, reorders all printers in the company. |
| `agentprinterIds` | yes | integer[] | |
```json
{
"printeragentId": 1,
"agentprinterIds": [
1
]
}
```
## Signals
On success, emits SSE signal: `agentprinter` (see [`GET /events`](../events.md)).
## Response `200`
Applied order
```json
{
"agentprinterIds": [
1
]
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid order or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -17,13 +17,13 @@ Prints a label for the article using a label template.
| Field | Required | Type | Description | | Field | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `templateId` | yes | integer | | | `templateId` | yes | integer | |
| `printerId` | yes | integer | | | `agentprinterId` | yes | integer | |
| `quantity` | yes | integer | | | `quantity` | yes | integer | |
```json ```json
{ {
"templateId": 1, "templateId": 1,
"printerId": 1, "agentprinterId": 1,
"quantity": 1 "quantity": 1
} }
``` ```

View File

@@ -2,11 +2,11 @@
Protected. Requires a valid session cookie (`credentials: 'include'`). Protected. Requires a valid session cookie (`credentials: 'include'`).
Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) Replace POS prices by SKU (net > gross)
Replace-all prices for the company POS customer group. Replace-all prices for the company POS customer group.
`net` takes precedence over `price` (legacy alias for net) and `gross`. `net` takes precedence over `gross`.
`tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax. `taxRate` specifies the tax rate percentage (e.g. 19 for 19%), not monetary tax.
When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`. When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`.
Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET). Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET).
Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
@@ -23,11 +23,9 @@ Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
| Field | Required | Type | Description | | Field | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `prices` | yes | object[] | | | `prices` | yes | object[] | |
| `prices[].net` | no | number | Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required. | | `prices[].net` | no | number | Net price (excl. VAT). Either `net` or `gross` is required. |
| `prices[].gross` | no | number | Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. | | `prices[].gross` | no | number | Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. |
| `prices[].price` | no | number | Legacy alias for `net`. |
| `prices[].taxRate` | no | number | Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). | | `prices[].taxRate` | no | number | Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). |
| `prices[].tax` | no | number | Alias for `taxRate` percentage. |
| `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request. | | `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request. |
```json ```json

View File

@@ -4,7 +4,8 @@ Public. No cookie required.
Login with Google ID token Login with Google ID token
Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`
or when the email has a pending auto-invitation),
stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie. stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie.
`user.picture` is the profile image as base64 (same as GET /auth/me). `user.picture` is the profile image as base64 (same as GET /auth/me).
@@ -43,7 +44,7 @@ Login successful; `token` cookie set
|--------|-------------| |--------|-------------|
| 400 | Missing token or Google client not configured — `{"error":"…"}` | | 400 | Missing token or Google client not configured — `{"error":"…"}` |
| 401 | Invalid Google token or account — `{"error":"…"}` | | 401 | Invalid Google token or account — `{"error":"…"}` |
| 403 | Email not on allowlist — `{"error":"…"}` | | 403 | Email not on allowlist or invited — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` | | 500 | Server error — `{"error":"…"}` |
--- ---

View File

@@ -43,6 +43,7 @@ Updated user
|--------|-------------| |--------|-------------|
| 400 | Invalid companyId — `{"error":"…"}` | | 400 | Invalid companyId — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (the user is not a member of this company) — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` | | 500 | Server error — `{"error":"…"}` |
--- ---

View File

@@ -0,0 +1,55 @@
# `POST /autoinvite`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Create auto-invitation
Registers an email for automatic membership. When a user with this email registers,
the invitation is removed from the list and the user is added to the company with the given role.
Emails that are already registered are rejected (409); use `POST /companyuser` for those. Admin only.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `email` | yes | string | |
| `role` | no | string | |
```json
{
"email": "newhire@example.com",
"role": "user"
}
```
## Signals
On success, emits SSE signal: `autoinvite` (see [`GET /events`](../events.md)).
## Response `201`
Created invitation
```json
{
"autoinvite": {
"id": 1,
"email": "…",
"role": "admin"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected, invalid email or invalid role — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
| 409 | User already registered or invitation already exists — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,36 @@
# `DELETE /autoinvite/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete auto-invitation
Deletes a pending auto-invitation of the selected company. Admin only.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Signals
On success, emits SSE signal: `autoinvite` (see [`GET /events`](../events.md)).
## Response `204`
Deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid id or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
| 404 | Invitation not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,36 @@
# `GET /autoinvite`
Protected. Requires a valid session cookie (`credentials: 'include'`).
List auto-invitations
Lists pending auto-invitations of the selected company. Admin only.
## Response `200`
Pending invitations
```json
{
"autoinvites": [
{
"id": 1,
"email": "…",
"role": "admin",
"createdAt": "…"
}
]
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -42,7 +42,8 @@ Created; selected company updated
"iso639": "…", "iso639": "…",
"taxrate": 1, "taxrate": 1,
"reducedtaxrate": 1, "reducedtaxrate": 1,
"taxfreerate": 1 "taxfreerate": 1,
"role": "admin"
}, },
"user": { "user": {
"id": 1, "id": 1,
@@ -59,6 +60,7 @@ Created; selected company updated
|--------|-------------| |--------|-------------|
| 400 | Validation error — `{"error":"…"}` | | 400 | Validation error — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (invited users cannot create companies when implicit create is disabled) — `{"error":"…"}` |
--- ---

View File

@@ -21,7 +21,8 @@ Company list
"iso639": "…", "iso639": "…",
"taxrate": 1, "taxrate": 1,
"reducedtaxrate": 1, "reducedtaxrate": 1,
"taxfreerate": 1 "taxfreerate": 1,
"role": "admin"
} }
] ]
} }

View File

@@ -48,7 +48,8 @@ Updated
"iso639": "…", "iso639": "…",
"taxrate": 1, "taxrate": 1,
"reducedtaxrate": 1, "reducedtaxrate": 1,
"taxfreerate": 1 "taxfreerate": 1,
"role": "admin"
} }
} }
``` ```

View File

@@ -0,0 +1,58 @@
# `POST /companyuser`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Add an existing user to the company
Adds an existing (registered) user, identified by email, to the selected company. Admin only.
The added user can then switch to this company via `PUT /auth/selected-company`.
For emails that are not registered yet use `POST /autoinvite`.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `email` | yes | string | |
| `role` | no | string | |
```json
{
"email": "colleague@example.com",
"role": "user"
}
```
## Signals
On success, emits SSE signal: `companyuser` (see [`GET /events`](../events.md)).
## Response `201`
Created company user
```json
{
"companyuser": {
"id": 1,
"googleAuthId": 1,
"email": "…",
"picture": "…",
"role": "admin"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected, invalid email or invalid role — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
| 404 | No registered user with this email — `{"error":"…"}` |
| 409 | User already in company — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,40 @@
# `DELETE /companyuser/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Remove a user from the company
Removes a member from the selected company. History entries stay but lose their author link;
if the removed user had this company selected, their selection is cleared.
The last admin cannot be removed. Admin only.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Signals
On success, emits SSE signal: `companyuser` (see [`GET /events`](../events.md)).
## Response `204`
Removed
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid id or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
| 404 | Company user not found — `{"error":"…"}` |
| 409 | Cannot remove the last admin — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,37 @@
# `GET /companyuser`
Protected. Requires a valid session cookie (`credentials: 'include'`).
List company users
Lists the members of the selected company with their role. Admin only.
## Response `200`
Company members
```json
{
"companyusers": [
{
"id": 1,
"googleAuthId": 1,
"email": "…",
"picture": "…",
"role": "admin"
}
]
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,59 @@
# `PATCH /companyuser/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Change a company user's role
Changes the role of a member of the selected company. The last admin cannot be demoted. Admin only.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `role` | yes | string | |
```json
{
"role": "admin"
}
```
## Signals
On success, emits SSE signal: `companyuser` (see [`GET /events`](../events.md)).
## Response `200`
Updated company user
```json
{
"companyuser": {
"id": 1,
"googleAuthId": 1,
"email": "…",
"picture": "…",
"role": "admin"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid id, role or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized (admin of the selected company required) — `{"error":"…"}` |
| 404 | Company user not found — `{"error":"…"}` |
| 409 | Cannot demote the last admin — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -46,6 +46,10 @@ emits `{ sender, brandId }`.
| `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` | | `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` |
| `labeltemplate` | `{}` | `POST /labeltemplate`, `PUT /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `DELETE /labeltemplate/{id}` | | `labeltemplate` | `{}` | `POST /labeltemplate`, `PUT /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `DELETE /labeltemplate/{id}` |
| `printer` | `{}` | `POST /printer`, `PUT /printer/{id}`, `PATCH /printer/{id}`, `DELETE /printer/{id}` | | `printer` | `{}` | `POST /printer`, `PUT /printer/{id}`, `PATCH /printer/{id}`, `DELETE /printer/{id}` |
| `printeragent` | `{}` | `PUT /printeragent`, `PATCH /printeragent/{id}`, `DELETE /printeragent/{id}`, printer agent connect/disconnect/state |
| `agentprinter` | `{ printers }` or `{}` | `PATCH /agentprinter/{id}`, `PUT /agentprinter/order`, `DELETE /agentprinter/{id}`, agent state updates / printer discovery |
| `companyuser` | `{}` | `POST /companyuser`, `PATCH /companyuser/{id}`, `DELETE /companyuser/{id}` |
| `autoinvite` | `{}` | `POST /autoinvite`, `DELETE /autoinvite/{id}` |
| `warehouse` | `{}` | `POST /warehouse`, `PUT /warehouse/{id}`, `PATCH /warehouse/{id}`, `DELETE /warehouse/{id}` | | `warehouse` | `{}` | `POST /warehouse`, `PUT /warehouse/{id}`, `PATCH /warehouse/{id}`, `DELETE /warehouse/{id}` |
| `supplier` | `{}` | `POST /supplier`, `PATCH /supplier/{id}`, `DELETE /supplier/{id}` | | `supplier` | `{}` | `POST /supplier`, `PATCH /supplier/{id}`, `DELETE /supplier/{id}` |
| `vfs` | `{}` | VFS uploads and filesystem mutations such as `POST /vfs/upload`, `POST /vfs/mkdir`, `POST /vfs/rename`, `PUT /vfs/{*path}`, `DELETE /vfs/{*path}` | | `vfs` | `{}` | VFS uploads and filesystem mutations such as `POST /vfs/upload`, `POST /vfs/mkdir`, `POST /vfs/rename`, `PUT /vfs/{*path}`, `DELETE /vfs/{*path}` |

View File

@@ -0,0 +1,57 @@
# `POST /labeltemplate/testprint`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Test print label template
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.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `agentprinterId` | yes | integer | Agent printer ID |
| `templateId` | no | integer | |
| `objects` | no | any[] | |
| `width` | no | integer | |
| `height` | no | integer | |
| `dpi` | no | integer | |
| `sku` | no | string | |
| `quantity` | no | integer | |
```json
{
"agentprinterId": 1,
"templateId": 1,
"objects": [],
"width": 1,
"height": 1,
"dpi": 1,
"sku": "…",
"quantity": 1
}
```
## Response `200`
Print job created
```json
{
"success": true
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid parameters |
| 404 | Printer or template not found |
| 500 | Server error |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,37 @@
# `GET /printer/{id}/state`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get live state for a printer
Retrieves live operational status for a single printer by configured printer ID,
serial number, or device identifier for the authenticated user's selected company.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | string | Configured printer ID, serial number, or device identifier |
## Response `200`
Live printer state
```json
{
"printer": {}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected or invalid ID — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 404 | Printer not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,39 @@
# `GET /printer/state`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get live printer states
Retrieves real-time operational status and telemetry for all discovered printers
reported by active printer agents for the authenticated user's selected company.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `serial` | no | string | Optional serial number filter |
| `agentId` | no | integer | Optional agent ID filter |
## Response `200`
Live printer states
```json
{
"printers": [
{}
]
}
```
## 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`._

View File

@@ -20,13 +20,11 @@ Printer agent list
"token": "…", "token": "…",
"pairingpin": "…", "pairingpin": "…",
"pairedAt": "…", "pairedAt": "…",
"clientserial": "…",
"clientmodel": "…",
"clientstate": "…",
"agentversion": "…", "agentversion": "…",
"lastseenAt": "…", "lastseenAt": "…",
"updatedAt": "…", "updatedAt": "…",
"sort": 1 "sort": 1,
"enabled": true
} }
] ]
} }

View File

@@ -14,6 +14,7 @@ The pairingpin is randomly generated server-side and returned in the response.
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `name` | yes | string | | | `name` | yes | string | |
| `sort` | no | integer | | | `sort` | no | integer | |
| `enabled` | no | boolean | |
```json ```json
{ {
@@ -34,13 +35,11 @@ Created printer agent with generated pairingpin
"token": "…", "token": "…",
"pairingpin": "…", "pairingpin": "…",
"pairedAt": "…", "pairedAt": "…",
"clientserial": "…",
"clientmodel": "…",
"clientstate": "…",
"agentversion": "…", "agentversion": "…",
"lastseenAt": "…", "lastseenAt": "…",
"updatedAt": "…", "updatedAt": "…",
"sort": 1 "sort": 1,
"enabled": true
} }
} }
``` ```

View File

@@ -18,11 +18,13 @@ Updates name and/or display order (sort) of a printer agent.
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `name` | no | string | | | `name` | no | string | |
| `sort` | no | integer | | | `sort` | no | integer | |
| `enabled` | no | boolean | |
```json ```json
{ {
"name": "…", "name": "…",
"sort": 1 "sort": 1,
"enabled": true
} }
``` ```
@@ -39,13 +41,11 @@ Updated printer agent
"token": "…", "token": "…",
"pairingpin": "…", "pairingpin": "…",
"pairedAt": "…", "pairedAt": "…",
"clientserial": "…",
"clientmodel": "…",
"clientstate": "…",
"agentversion": "…", "agentversion": "…",
"lastseenAt": "…", "lastseenAt": "…",
"updatedAt": "…", "updatedAt": "…",
"sort": 1 "sort": 1,
"enabled": true
} }
} }
``` ```

View File

@@ -0,0 +1,46 @@
# `GET /printerjob`
Protected. Requires a valid session cookie (`credentials: 'include'`).
List printer jobs
Lists printer jobs for the selected company, optionally filtered by agent printer or state.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `agentprinterId` | no | integer | Optional agent printer ID filter |
| `state` | no | string | Optional job state filter |
## Response `200`
List of printer jobs
```json
{
"printerjobs": [
{
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
]
}
```
## 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`._

View File

@@ -0,0 +1,46 @@
# `POST /printerjob`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Create print job
Creates a new print job for an agent printer. Notifies the agent in real time.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `agentprinterId` | yes | integer | |
| `type` | no | string | |
| `data` | yes | string | |
```json
{
"agentprinterId": 1,
"type": "zpl",
"data": "…"
}
```
## Response `201`
Printer job created
```json
{
"printerjob": {
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
}
```
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,39 @@
# `DELETE /printerjob/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Cancel and delete printer job
Cancels and deletes a print job. If the job was pending or processing, sends a cancellation
event to the connected printer agent before deletion.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Response `200`
Job cancelled and deleted
```json
{
"success": true,
"deletedJob": {
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
}
```
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,44 @@
# `GET /printerjob/{id}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get printer job
Retrieves details of a specific printer job by ID.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Response `200`
Printer job details
```json
{
"printerjob": {
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 404 | Printer job not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,37 @@
# `POST /printerjob/{id}/cancel`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Cancel printer job
Marks a print job as cancelled and notifies the connected agent in real time.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | |
## Response `200`
Printer job cancelled
```json
{
"success": true,
"printerjob": {
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
}
```
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,48 @@
# `POST /printerjob/cancel-all`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Cancel all pending and processing printer jobs
Cancels all queued or in-flight print jobs for the selected company, optionally filtered by agentprinterId.
Notifies connected agents in real time.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `agentprinterId` | no | integer | |
```json
{
"agentprinterId": 1
}
```
## Response `200`
All matching jobs cancelled
```json
{
"success": true,
"cancelledCount": 1,
"jobs": [
{
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
]
}
```
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

56
docs/api/routes/zpljob.md Normal file
View File

@@ -0,0 +1,56 @@
# `POST /zpljob`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Create ZPL print job
Issues a new ZPL print job to an agent printer. If copiescount > 1, the ZPL quantity command (^PQ)
is automatically applied. The connected printer agent receives an immediate real-time event.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `agentprinterId` | yes | integer | Target agent printer ID |
| `copiescount` | no | integer | Optional copy count (defaults to 1) |
| `data` | yes | string | Raw ZPL string |
```json
{
"agentprinterId": 1,
"copiescount": 2,
"data": "^XA^FO50,50^ADN,36,20^FDHello Zebra^FS^XZ"
}
```
## Response `201`
Print job created
```json
{
"printerjob": {
"id": 1,
"agentprinterId": 1,
"printeragentId": 1,
"type": "zpl",
"state": "pending",
"data": "…",
"createdAt": "2026-08-10T00:00:00.000Z",
"completedAt": "2026-08-10T00:00:00.000Z"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid input or missing fields — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 404 | Agent printer not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

874
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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", "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,8 +57,7 @@
"swagger-jsdoc": "^6.3.0" "swagger-jsdoc": "^6.3.0"
}, },
"allowScripts": { "allowScripts": {
"github:sebseb7/react-canvas-label-editor#7f0e31b66ff22da011e872b201017398b14e9dd9": true, "react-canvas-label-editor@20.0.0": true,
"canvas@3.2.3": true,
"better-sqlite3@13.0.3": true "better-sqlite3@13.0.3": true
}, },
"overrides": { "overrides": {

View File

@@ -39,13 +39,16 @@ const OMITTED_ROUTES = new Set([
'POST /serper/image', 'POST /serper/image',
'POST /serper/images', 'POST /serper/images',
'GET /shipment/{id}/file/{fileId}', 'GET /shipment/{id}/file/{fileId}',
'GET /smallthumb/{fullHash}',
'GET /thumb/{fullHash}', 'GET /thumb/{fullHash}',
'GET /v1/category', 'GET /v1/category',
'GET /v1/cimage', 'GET /v1/cimage',
'GET /v1/client', 'GET /v1/client',
'GET /v1/customer', 'GET /v1/customer',
'GET /v1/customergroup', 'GET /v1/customergroup',
'GET /v1/deletedentity',
'GET /v1/init', 'GET /v1/init',
'POST /v1/order',
'GET /v1/pimage', 'GET /v1/pimage',
'GET /v1/product', 'GET /v1/product',
]); ]);
@@ -81,6 +84,36 @@ function isRouteOmitted(method, apiPath) {
if (apiPath === '/auth' || apiPath.startsWith('/auth/')) { if (apiPath === '/auth' || apiPath.startsWith('/auth/')) {
return true; return true;
} }
if (apiPath === '/agent' || apiPath.startsWith('/agent/')) {
return true;
}
if (apiPath === '/agentprinter' || apiPath.startsWith('/agentprinter/')) {
return true;
}
if (apiPath === '/printer' || apiPath.startsWith('/printer/')) {
return true;
}
if (apiPath === '/printeragent' || apiPath.startsWith('/printeragent/')) {
return true;
}
if (apiPath === '/printerjob' || apiPath.startsWith('/printerjob/')) {
return true;
}
if (apiPath === '/smallthumb' || apiPath.startsWith('/smallthumb/')) {
return true;
}
if (apiPath === '/v1' || apiPath.startsWith('/v1/')) {
return true;
}
if (apiPath === '/health' || apiPath.startsWith('/health/')) {
return true;
}
if (apiPath === '/subagentschedule' || apiPath.startsWith('/subagentschedule/')) {
return true;
}
if (apiPath === '/zpljob' || apiPath.startsWith('/zpljob/')) {
return true;
}
return OMITTED_ROUTES.has(`${method.toUpperCase()} ${apiPath}`); return OMITTED_ROUTES.has(`${method.toUpperCase()} ${apiPath}`);
} }

View File

@@ -1,8 +1,23 @@
import express from 'express'; import express from 'express';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import * as db from '../../db/printeragent.js'; import * as db from '../../db/printeragent.js';
import * as agentprinterDb from '../../db/agentprinter.js';
import * as printerjobDb from '../../db/printerjob.js';
import { agentAuthMiddleware, extractAgentVersion } from '../middleware/agentAuth.js'; import { agentAuthMiddleware, extractAgentVersion } from '../middleware/agentAuth.js';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { createLogger } from '../../util/logger.js';
import {
enrichAgentWithState,
getAgentState,
setAgentState,
} from './stateStore.js';
import {
updateAgentPrinters,
removeAgentPrinters,
getCompanyPrinters,
} from '../../services/companyPrinterService.js';
const log = createLogger('agent');
export const agentRouter = express.Router(); export const agentRouter = express.Router();
@@ -188,6 +203,41 @@ async function handlePair(req, res) {
agentRouter.post('/pair', handlePair); agentRouter.post('/pair', handlePair);
agentRouter.get('/pair', handlePair); agentRouter.get('/pair', handlePair);
const activeAgentEvents = new Map();
/**
* Notifies a connected agent of a new or cancelled printer job.
* @param {number} agentId
* @param {'created'|'cancelled'} action
* @param {object} job
* @returns {boolean} True if delivered to at least one active agent connection.
*/
export function notifyAgentJob(agentId, action, job) {
const aId = Number(agentId);
const conns = activeAgentEvents.get(aId);
const count = conns ? conns.size : 0;
log.info(`notifyAgentJob: agentId=${aId} action=${action} jobId=${job?.id} activeConnections=${count}`);
if (!conns || conns.size === 0) {
log.warn(`notifyAgentJob: No active SSE connection for agent ${aId} (jobId=${job?.id}); agent must poll/reconnect`);
return false;
}
const payload = JSON.stringify({
action,
jobId: job.id,
agentprinterId: job.agentprinterId,
type: job.type,
state: job.state,
createdAt: job.createdAt,
});
for (const clientRes of conns) {
if (!clientRes.writableEnded) {
clientRes.write(`event: job\ndata: ${payload}\n\n`);
clientRes.flush?.();
}
}
return true;
}
/** /**
* @openapi * @openapi
* /agent/events: * /agent/events:
@@ -224,7 +274,7 @@ agentRouter.get('/pair', handlePair);
agentRouter.get('/events', agentAuthMiddleware, async (req, res) => { agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
const version = extractAgentVersion(req); const version = extractAgentVersion(req);
if (version) { if (version) {
db.touchLastseen(req.agent.id, version).catch(() => {}); db.touchLastseen(req.agent.id, version).catch(() => { });
} }
res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Content-Type', 'text/event-stream');
@@ -233,16 +283,47 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
res.setHeader('X-Accel-Buffering', 'no'); res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders?.(); res.flushHeaders?.();
const agentVersion = version || req.agent?.agentversion;
const versionSuffix = agentVersion ? ` v${agentVersion}` : '';
const agentName = req.agent?.name || 'unnamed';
log.info(`Agent connected: id=${req.agent.id} ("${agentName}")${versionSuffix}`);
if (!getAgentState(req.agent.id)) {
setAgentState(req.agent.id, 'online');
}
const aId = Number(req.agent.id);
if (!activeAgentEvents.has(aId)) {
activeAgentEvents.set(aId, new Set());
}
activeAgentEvents.get(aId).add(res);
res.write(`event: connected\ndata: ${JSON.stringify({ agentId: req.agent.id, name: req.agent.name })}\n\n`); res.write(`event: connected\ndata: ${JSON.stringify({ agentId: req.agent.id, name: req.agent.name })}\n\n`);
const keepAlive = setInterval(() => { const keepAlive = setInterval(() => {
if (res.writableEnded) return; if (res.writableEnded) return;
res.write(': heartbeat\n\n'); res.write(': heartbeat\n\n');
db.touchLastseen(req.agent.id).catch(() => {}); db.touchLastseen(req.agent.id).catch(() => { });
}, 15000); }, 15000);
let closed = false;
const cleanup = () => { const cleanup = () => {
if (closed) return;
closed = true;
clearInterval(keepAlive); clearInterval(keepAlive);
const conns = activeAgentEvents.get(aId);
if (conns) {
conns.delete(res);
if (conns.size === 0) activeAgentEvents.delete(aId);
}
log.info(`Agent disconnected: id=${req.agent.id} ("${agentName}")`);
setAgentState(req.agent.id, 'offline');
if (req.agent?.companyId) {
removeAgentPrinters(req.agent.companyId, req.agent.id);
emit('printeragent', { companyId: req.agent.companyId });
}
}; };
req.on('close', cleanup); req.on('close', cleanup);
@@ -286,30 +367,154 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
*/ */
async function handleState(req, res) { async function handleState(req, res) {
try { try {
const rawState = req.body?.state !== undefined ? req.body.state : (req.body?.clientstate !== undefined ? req.body.clientstate : req.body); const rawState = req.body?.state;
const stateStr = rawState == null ? null : (typeof rawState === 'string' ? rawState : JSON.stringify(rawState)); setAgentState(req.agent.id, rawState);
const updated = await db.updateState(req.agent.id, stateStr);
const version = extractAgentVersion(req); const version = extractAgentVersion(req);
if (version) { if (version) {
await db.touchLastseen(req.agent.id, version); await db.touchLastseen(req.agent.id, version);
} else {
await db.touchLastseen(req.agent.id);
} }
if (updated.companyId) { const rawPrinters = Array.isArray(rawState?.printers) ? rawState.printers : null;
emit('printeragent', { companyId: updated.companyId });
let anyAgentPrinterCreated = false;
if (rawPrinters && rawPrinters.length > 0) {
for (const p of rawPrinters) {
try {
const result = await agentprinterDb.findOrCreate(req.agent.id, {
type: p.type || 'zpl',
model: p.model || null,
serial: p.serial || null,
});
if (result?.created) {
anyAgentPrinterCreated = true;
} }
if (result?.agentprinter) {
p.agentprinterId = result.agentprinter.id;
p.enabled = result.agentprinter.enabled;
p.sort = result.agentprinter.sort;
}
} catch (err) {
log.warn(`Failed to record agentprinter for agent ${req.agent.id}:`, err);
}
}
}
if (rawState?.job?.id && rawState?.job?.state) {
try {
await printerjobDb.updateState(
{ agentId: req.agent.id },
rawState.job.id,
rawState.job.state
);
} catch (jobErr) {
log.debug(`Agent reported job state update for ${rawState.job.id}:`, jobErr?.message);
}
}
if (rawPrinters && req.agent.companyId) {
updateAgentPrinters(req.agent.companyId, req.agent.id, req.agent.name, rawPrinters);
} else if (anyAgentPrinterCreated && req.agent.companyId) {
emit('agentprinter', { companyId: req.agent.companyId });
}
if (req.agent.companyId) {
emit('printeragent', { companyId: req.agent.companyId });
}
const agent = enrichAgentWithState({
...req.agent,
agentversion: version || req.agent.agentversion,
lastseenAt: new Date().toISOString(),
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
agent: updated, agent,
}); });
} catch (err) { } catch (err) {
console.error('Agent post state failed:', err); log.error('Agent post state failed:', err);
return res.status(500).json({ error: 'Internal server error' }); return res.status(500).json({ error: 'Internal server error' });
} }
} }
agentRouter.post('/state', agentAuthMiddleware, handleState); agentRouter.post('/state', agentAuthMiddleware, handleState);
agentRouter.put('/state', agentAuthMiddleware, handleState); agentRouter.put('/state', agentAuthMiddleware, handleState);
agentRouter.get('/state', agentAuthMiddleware, (req, res) => {
const memoryState = getAgentState(req.agent.id);
const companyPrinters = req.agent.companyId ? getCompanyPrinters(req.agent.companyId) : [];
const agentPrinters = companyPrinters.filter((p) => p.agentId === req.agent.id);
res.status(200).json({
state: memoryState ?? null,
printers: agentPrinters,
});
});
// #region Agent Jobs
agentRouter.get('/jobs', agentAuthMiddleware, async (req, res) => {
try {
const jobs = await printerjobDb.list({ agentId: req.agent.id }, req.query);
res.status(200).json({ jobs });
} catch (err) {
log.error('Agent get jobs failed:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
agentRouter.get('/jobs/:id', agentAuthMiddleware, async (req, res) => {
try {
const job = await printerjobDb.getById({ agentId: req.agent.id }, req.params.id);
if (!job) {
return res.status(404).json({ error: 'Printerjob not found' });
}
res.status(200).json({ job });
} catch (err) {
log.error('Agent get job failed:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
agentRouter.post('/jobs/:id/processing', agentAuthMiddleware, async (req, res) => {
try {
const job = await printerjobDb.updateState({ agentId: req.agent.id }, req.params.id, 'processing');
res.status(200).json({ success: true, job });
} catch (err) {
if (err.message === 'Printerjob not found') return res.status(404).json({ error: err.message });
log.error('Agent set job processing failed:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
agentRouter.post('/jobs/:id/completed', agentAuthMiddleware, async (req, res) => {
try {
const job = await printerjobDb.updateState({ agentId: req.agent.id }, req.params.id, 'completed');
res.status(200).json({ success: true, job });
} catch (err) {
if (err.message === 'Printerjob not found') return res.status(404).json({ error: err.message });
log.error('Agent set job completed failed:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
agentRouter.patch('/jobs/:id', agentAuthMiddleware, async (req, res) => {
const state = req.body?.state;
if (!state) {
return res.status(400).json({ error: 'state is required' });
}
try {
const job = await printerjobDb.updateState({ agentId: req.agent.id }, req.params.id, state);
res.status(200).json({ success: true, job });
} catch (err) {
if (err.message === 'Printerjob not found') return res.status(404).json({ error: err.message });
if (err.message?.startsWith('Invalid printerjob state')) return res.status(400).json({ error: err.message });
log.error('Agent patch job failed:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// #endregion

View File

@@ -0,0 +1,75 @@
/**
* In-memory state store for connected printer agents.
* Keeps transient agent telemetry out of the database.
*/
/** @type {Map<number, any>} */
const agentStates = new Map();
/**
* Sets the current telemetry/state for an agent.
* @param {number} agentId
* @param {any} state
*/
export function setAgentState(agentId, state) {
if (agentId == null) return;
agentStates.set(Number(agentId), state);
}
/**
* Gets the raw in-memory state for an agent.
* @param {number} agentId
* @returns {any}
*/
export function getAgentState(agentId) {
if (agentId == null) return undefined;
return agentStates.get(Number(agentId));
}
/**
* Formats a state value into a concise clientstate string suitable for display (e.g. 'online', 'offline').
* @param {any} state
* @returns {string|null}
*/
export function formatClientState(state) {
if (state == null) return null;
if (typeof state === 'string') return state;
if (typeof state === 'object') {
if (typeof state.status === 'string' && state.status.trim()) {
return state.status.trim();
}
if (typeof state.state === 'string' && state.state.trim()) {
return state.state.trim();
}
}
return JSON.stringify(state);
}
/**
* Removes in-memory state for an agent (e.g. when deleted).
* @param {number} agentId
*/
export function removeAgentState(agentId) {
if (agentId == null) return;
agentStates.delete(Number(agentId));
}
/**
* Enriches a printeragent object with the in-memory state.
* @template {object} T
* @param {T} agent
* @returns {T}
*/
export function enrichAgentWithState(agent) {
if (!agent || typeof agent !== 'object') return agent;
const memoryState = getAgentState(agent.id);
if (memoryState !== undefined) {
return {
...agent,
state: memoryState,
clientstate: formatClientState(memoryState),
};
}
return agent;
}

View File

@@ -106,7 +106,7 @@ function corsMiddleware(req, res, next) {
const reqHeaders = req.headers['access-control-request-headers']; const reqHeaders = req.headers['access-control-request-headers'];
res.setHeader( res.setHeader(
'Access-Control-Allow-Headers', 'Access-Control-Allow-Headers',
reqHeaders || 'Content-Type, Authorization, x-agent-token, x-auth-token, authtoken, auth-token, token, x-token, agentversion, clientversion, version' reqHeaders || 'Content-Type, Authorization, x-agent-token, agentversion'
); );
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
@@ -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 {

View File

@@ -10,16 +10,12 @@ export function extractAgentToken(req) {
return trimmed; return trimmed;
} }
for (const key of ['x-agent-token', 'x-auth-token', 'authtoken', 'auth-token', 'token', 'x-token']) { if (typeof req.headers['x-agent-token'] === 'string' && req.headers['x-agent-token'].trim()) {
const val = req.headers[key]; return req.headers['x-agent-token'].trim();
if (typeof val === 'string' && val.trim()) {
return val.trim();
}
} }
const queryToken = req.query?.authToken || req.query?.token; if (typeof req.query?.token === 'string' && req.query.token.trim()) {
if (typeof queryToken === 'string' && queryToken.trim()) { return req.query.token.trim();
return queryToken.trim();
} }
return ''; return '';
@@ -27,13 +23,8 @@ export function extractAgentToken(req) {
export function extractAgentVersion(req) { export function extractAgentVersion(req) {
const raw = req.headers['agentversion'] || const raw = req.headers['agentversion'] ||
req.headers['agent-version'] ||
req.headers['clientversion'] ||
req.headers['version'] ||
req.query?.agentversion || req.query?.agentversion ||
req.query?.version ||
req.body?.agentversion || req.body?.agentversion ||
req.body?.version ||
null; null;
return raw ? String(raw).trim() : null; return raw ? String(raw).trim() : null;
} }

View File

@@ -63,7 +63,7 @@ export const authMiddleware = async (req, res, next) => {
return next(); return next();
} }
if (req.path.startsWith('/api/agent')) { if (req.path === '/api/agent' || req.path.startsWith('/api/agent/')) {
return next(); return next();
} }

View File

@@ -59,6 +59,39 @@
* type: number * type: number
* taxfreerate: * taxfreerate:
* type: number * type: number
* role:
* type: string
* enum: [admin, user]
* description: The caller's role in this company
* CompanyUser:
* type: object
* required: [id, googleAuthId, email, role]
* properties:
* id:
* type: integer
* googleAuthId:
* type: integer
* email:
* type: string
* picture:
* type: string
* nullable: true
* role:
* type: string
* enum: [admin, user]
* AutoInvite:
* type: object
* required: [id, email, role, createdAt]
* properties:
* id:
* type: integer
* email:
* type: string
* role:
* type: string
* enum: [admin, user]
* createdAt:
* type: string
* SuccessResponse: * SuccessResponse:
* type: object * type: object
* required: [ok] * required: [ok]
@@ -1735,15 +1768,6 @@
* pairedAt: * pairedAt:
* type: string * type: string
* nullable: true * nullable: true
* clientserial:
* type: string
* nullable: true
* clientmodel:
* type: string
* nullable: true
* clientstate:
* type: string
* nullable: true
* agentversion: * agentversion:
* type: string * type: string
* nullable: true * nullable: true
@@ -1756,6 +1780,86 @@
* type: integer * type: integer
* description: Display order (ascending) * description: Display order (ascending)
* default: 0 * default: 0
* enabled:
* type: boolean
* default: true
* Agentprinter:
* type: object
* required: [id, printeragentId, type, enabled, sort]
* properties:
* id:
* type: integer
* printeragentId:
* type: integer
* type:
* type: string
* description: Printer type ("zpl", "esc/pos", etc.)
* model:
* type: string
* nullable: true
* serial:
* type: string
* nullable: true
* enabled:
* type: boolean
* default: true
* sort:
* type: integer
* description: Display order (ascending)
* default: 0
* open:
* type: boolean
* description: Whether the printer head/cover is open
* default: false
* pause:
* type: boolean
* description: Whether the printer is paused
* default: false
* error:
* type: boolean
* description: Whether the printer is in an error state
* default: false
* online:
* type: boolean
* description: Whether the printer is online and reachable
* default: false
* labelLength:
* type: integer
* nullable: true
* description: Discovered label length in dots/pixels reported by the printer
* default: null
* Printerjob:
* type: object
* required: [id, agentprinterId, type, state, data, createdAt]
* properties:
* id:
* type: integer
* agentprinterId:
* type: integer
* description: Target agent printer ID
* printeragentId:
* type: integer
* nullable: true
* description: Parent printer agent ID
* type:
* type: string
* description: Job format type ("zpl", "pdf", "epos")
* example: "zpl"
* state:
* type: string
* enum: [pending, processing, completed, cancelled, failed]
* description: Current processing state of the job
* example: "pending"
* data:
* type: string
* description: Raw job payload
* createdAt:
* type: string
* format: date-time
* completedAt:
* type: string
* format: date-time
* nullable: true
* LabelObject: * LabelObject:
* type: object * type: object
* required: [id, type, x, y] * required: [id, type, x, y]

View File

@@ -44,6 +44,7 @@ export const openApiOptions = {
{ name: 'Posterminal', description: 'Company-scoped POS terminals' }, { name: 'Posterminal', description: 'Company-scoped POS terminals' },
{ name: 'Printer', description: 'Company-scoped receipt and label printers' }, { name: 'Printer', description: 'Company-scoped receipt and label printers' },
{ name: 'Printeragent', description: 'Company-scoped printer agents (create, rename, delete)' }, { name: 'Printeragent', description: 'Company-scoped printer agents (create, rename, delete)' },
{ name: 'Agentprinter', description: 'Company-scoped agent printers (enable, sort, delete)' },
{ name: 'Agent', description: 'Printer agent device endpoints (pairing, SSE events, state)' }, { name: 'Agent', description: 'Printer agent device endpoints (pairing, SSE events, state)' },
{ name: 'Ledger', description: 'Append-only ledger transactions' }, { name: 'Ledger', description: 'Append-only ledger transactions' },
{ name: 'Stock', description: 'Manual stock additions and removals' }, { name: 'Stock', description: 'Manual stock additions and removals' },

View File

@@ -0,0 +1,413 @@
import * as db from '../../db/agentprinter.js';
import { emit } from '../sse.js';
import { enrichAgentPrinter } from '../../services/companyPrinterService.js';
// #region Helpers
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 agentprinterErrorStatus(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 === 'Printeragent not found') return 404;
if (error.message === 'Agentprinter not found') return 404;
if (error.message === 'Invalid agentprinter') return 400;
if (error.message === 'Invalid agentprinter order') return 400;
return 500;
}
// #endregion
/**
* Registers agentprinter routes for the authenticated user's selected company.
* @param {import('express').Application} app - Express app
* @param {string} [basePath=''] - Optional URL prefix for all routes in this module
*/
export function register(app, basePath = '') {
/**
* @openapi
* /agentprinter:
* get:
* tags: [Agentprinter]
* summary: List agent printers
* description: |
* Lists agent printers for the authenticated user's selected company,
* optionally filtered by printer agent ID.
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: printeragentId
* schema:
* type: integer
* description: Optional printer agent ID filter
* responses:
* 200:
* description: List of agent printers
* content:
* application/json:
* schema:
* type: object
* properties:
* agentprinters:
* type: array
* items:
* $ref: '#/components/schemas/Agentprinter'
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /agentprinter
app.get(`${basePath}/agentprinter`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const printeragentId = req.query?.printeragentId ? Number(req.query.printeragentId) : undefined;
const agentprinters = await db.list(req.user.id, printeragentId);
const enriched = agentprinters.map((p) => enrichAgentPrinter(selected.companyId, p));
res.status(200).json({ agentprinters: enriched });
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('List agentprinters failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /agentprinter/order:
* put:
* tags: [Agentprinter]
* summary: Reorder agent printers
* description: |
* Reorders agent printers for a specific printer agent within the selected company.
* security:
* - cookieAuth: []
* x-signals: [agentprinter]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [agentprinterIds]
* properties:
* printeragentId:
* type: integer
* description: Optional agent id. If omitted, reorders all printers in the company.
* agentprinterIds:
* type: array
* items:
* type: integer
* responses:
* 200:
* description: Applied order
* content:
* application/json:
* schema:
* type: object
* properties:
* agentprinterIds:
* type: array
* items:
* type: integer
* 400:
* description: Invalid order or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region PUT /agentprinter/order
app.put(`${basePath}/agentprinter/order`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const { printeragentId, agentprinterIds } = req.body ?? {};
try {
const result = await db.order(req.user.id, { printeragentId, agentprinterIds });
emit('agentprinter', { companyId: selected.companyId });
res.status(200).json(result);
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('Order agentprinters failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /agentprinter/{id}:
* get:
* tags: [Agentprinter]
* summary: Get agent printer
* description: Retrieves a single agent printer by ID.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Agent printer details
* content:
* application/json:
* schema:
* type: object
* properties:
* agentprinter:
* $ref: '#/components/schemas/Agentprinter'
* 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: Agent printer not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /agentprinter/:id
app.get(`${basePath}/agentprinter/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
}
try {
const agentprinter = await db.getById(req.user.id, parsed.id);
if (!agentprinter) {
return res.status(404).json({ error: 'Agentprinter not found' });
}
res.status(200).json({ agentprinter: enrichAgentPrinter(selected.companyId, agentprinter) });
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('Get agentprinter failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /agentprinter/{id}:
* patch:
* tags: [Agentprinter]
* summary: Update agent printer
* description: |
* Updates enabled and/or display order (sort) of an agent printer.
* security:
* - cookieAuth: []
* x-signals: [agentprinter]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* enabled:
* type: boolean
* sort:
* type: integer
* responses:
* 200:
* description: Updated agent printer
* content:
* application/json:
* schema:
* type: object
* properties:
* agentprinter:
* $ref: '#/components/schemas/Agentprinter'
* 400:
* description: Invalid input 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: Agent printer not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region PATCH /agentprinter/:id
app.patch(`${basePath}/agentprinter/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
}
const payload = { id: parsed.id };
if (req.body?.enabled !== undefined) payload.enabled = Boolean(req.body.enabled);
if (req.body?.sort !== undefined) {
const sort = Number(req.body.sort);
if (!Number.isInteger(sort)) {
return res.status(400).json({ error: 'sort must be an integer' });
}
payload.sort = sort;
}
if (payload.enabled === undefined && payload.sort === undefined) {
return res.status(400).json({ error: 'At least one field (enabled, sort) is required' });
}
try {
const agentprinter = await db.update(req.user.id, payload);
emit('agentprinter', { companyId: selected.companyId });
res.status(200).json({ agentprinter: enrichAgentPrinter(selected.companyId, agentprinter) });
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('Update agentprinter failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /agentprinter/{id}:
* delete:
* tags: [Agentprinter]
* summary: Delete agent printer
* description: Deletes an agent printer from the selected company.
* security:
* - cookieAuth: []
* x-signals: [agentprinter]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 204:
* description: Agent printer 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: Agent printer not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /agentprinter/:id
app.delete(`${basePath}/agentprinter/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
}
try {
await db.remove(req.user.id, parsed.id);
emit('agentprinter', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('Delete agentprinter failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -17,11 +17,11 @@ export function register(app, basePath = '') {
* /article/{sku}/prices: * /article/{sku}/prices:
* put: * put:
* tags: [Article] * tags: [Article]
* summary: Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) * summary: Replace POS prices by SKU (net > gross)
* description: | * description: |
* Replace-all prices for the company POS customer group. * Replace-all prices for the company POS customer group.
* `net` takes precedence over `price` (legacy alias for net) and `gross`. * `net` takes precedence over `gross`.
* `tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax. * `taxRate` specifies the tax rate percentage (e.g. 19 for 19%), not monetary tax.
* When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`. * When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`.
* Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET). * Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET).
* Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. * Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
@@ -50,19 +50,13 @@ export function register(app, basePath = '') {
* properties: * properties:
* net: * net:
* type: number * type: number
* description: Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required. * description: Net price (excl. VAT). Either `net` or `gross` is required.
* gross: * gross:
* type: number * type: number
* description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. * description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`.
* price:
* type: number
* description: Legacy alias for `net`.
* taxRate: * taxRate:
* type: number * type: number
* description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). * description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%).
* tax:
* type: number
* description: Alias for `taxRate` percentage.
* minmultiplier: * minmultiplier:
* type: number * type: number
* description: Defaults to 1; must be unique per request. * description: Defaults to 1; must be unique per request.

View File

@@ -1,10 +1,12 @@
import fs from 'fs/promises';
import path from 'path';
import express from 'express'; import express from 'express';
import { renderLabel } from 'react-canvas-label-editor'; import { renderLabel } from 'react-canvas-label-editor';
import * as db from '../../../db/article.js'; import * as db from '../../../db/article.js';
import * as printerDb from '../../../db/printer.js'; import * as agentprinterDb from '../../../db/agentprinter.js';
import * as labeltemplateDb from '../../../db/labeltemplate.js'; import * as labeltemplateDb from '../../../db/labeltemplate.js';
import * as printerjobDb from '../../../db/printerjob.js';
import { notifyAgentJob } from '../../agent/index.js';
import { pngToZpl } from '../../../services/zplService.js';
import { applyZplCopies } from '../zpljob.js';
import { import {
AVIF_BODY_LIMIT, AVIF_BODY_LIMIT,
emitArticleSignal, emitArticleSignal,
@@ -12,6 +14,8 @@ import {
parseFullHash, parseFullHash,
articleErrorStatus, articleErrorStatus,
interpolateString, interpolateString,
withMandatedOptionsPrice,
interpolateObjects,
} from './shared.js'; } from './shared.js';
/** /**
@@ -325,11 +329,11 @@ export function register(app, basePath = '') {
* application/json: * application/json:
* schema: * schema:
* type: object * type: object
* required: [templateId, printerId, quantity] * required: [templateId, agentprinterId, quantity]
* properties: * properties:
* templateId: * templateId:
* type: integer * type: integer
* printerId: * agentprinterId:
* type: integer * type: integer
* quantity: * quantity:
* type: integer * type: integer
@@ -364,7 +368,8 @@ export function register(app, basePath = '') {
if (skuError) return res.status(400).json({ error: skuError }); if (skuError) return res.status(400).json({ error: skuError });
const sku = parsedSku; const sku = parsedSku;
const { templateId, printerId, quantity } = req.body ?? {}; const { templateId, quantity } = req.body ?? {};
const agentprinterId = req.body?.agentprinterId ?? req.body?.printerId;
if (templateId === undefined || templateId === null) { if (templateId === undefined || templateId === null) {
return res.status(400).json({ error: 'templateId is required' }); return res.status(400).json({ error: 'templateId is required' });
@@ -373,11 +378,11 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: 'templateId must be an integer' }); return res.status(400).json({ error: 'templateId must be an integer' });
} }
if (printerId === undefined || printerId === null) { if (agentprinterId === undefined || agentprinterId === null) {
return res.status(400).json({ error: 'printerId is required' }); return res.status(400).json({ error: 'agentprinterId is required' });
} }
if (typeof printerId !== 'number' || !Number.isInteger(printerId) || printerId <= 0) { if (typeof agentprinterId !== 'number' || !Number.isInteger(agentprinterId) || agentprinterId <= 0) {
return res.status(400).json({ error: 'printerId must be an integer' }); return res.status(400).json({ error: 'agentprinterId must be an integer' });
} }
if (quantity === undefined || quantity === null) { if (quantity === undefined || quantity === null) {
@@ -387,20 +392,16 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: 'quantity must be an integer' }); return res.status(400).json({ error: 'quantity must be an integer' });
} }
const parsedPrinterId = { id: printerId };
const parsedTemplateId = { id: templateId };
const parsedQuantity = { id: quantity };
try { try {
const printer = await printerDb.getById(req.user.id, parsedPrinterId.id); const printer = await agentprinterDb.getById(req.user.id, agentprinterId);
if (!printer || printer.company_id !== companyId) { if (!printer) {
return res.status(404).json({ error: 'Printer not found' }); return res.status(404).json({ error: 'Printer not found' });
} }
if (printer.type !== 'zpl') { if (printer.type && printer.type.toLowerCase() !== 'zpl') {
return res.status(400).json({ error: 'Printer is not a ZPL printer' }); return res.status(400).json({ error: 'Printer is not a ZPL printer' });
} }
const template = await labeltemplateDb.getById(req.user.id, parsedTemplateId.id); const template = await labeltemplateDb.getById(req.user.id, templateId);
if (!template || template.company_id !== companyId) { if (!template || template.company_id !== companyId) {
return res.status(404).json({ error: 'Label template not found' }); return res.status(404).json({ error: 'Label template not found' });
} }
@@ -408,7 +409,7 @@ export function register(app, basePath = '') {
let article; let article;
try { try {
const result = await db.getBySku(req.user.id, sku); const result = await db.getBySku(req.user.id, sku);
article = result?.article; article = result?.article ? await withMandatedOptionsPrice(req.user.id, result.article) : undefined;
} catch (err) { } catch (err) {
const status = articleErrorStatus(err); const status = articleErrorStatus(err);
if (status === 500) console.error('Get article by sku failed for print:', err); if (status === 500) console.error('Get article by sku failed for print:', err);
@@ -418,24 +419,24 @@ export function register(app, basePath = '') {
return res.status(404).json({ error: 'Article not found' }); return res.status(404).json({ error: 'Article not found' });
} }
// Extract and interpolate canvas objects // Extract and interpolate canvas objects or raw ZPL
const content = template.content; const content = template.content;
let zplData;
if (typeof content === 'string' && content.includes('^XA')) {
let interpolated = interpolateString(content, article);
if (quantity > 1) {
interpolated = applyZplCopies(interpolated, quantity);
}
zplData = interpolated;
} else {
const objects = Array.isArray(content?.objects) const objects = Array.isArray(content?.objects)
? content.objects ? content.objects
: Array.isArray(content) : Array.isArray(content)
? content ? content
: []; : [];
const interpolatedObjects = objects.map(obj => { const interpolatedObjects = interpolateObjects(objects, article);
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({ const printPayload = await renderLabel({
width: template.width, width: template.width,
@@ -443,18 +444,20 @@ export function register(app, basePath = '') {
objects: interpolatedObjects, objects: interpolatedObjects,
}); });
// Mock network dispatch zplData = await pngToZpl(printPayload, quantity);
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 }); const job = await printerjobDb.create(req.user.id, {
agentprinterId: printer.id,
type: 'zpl',
data: zplData,
});
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'created', job);
}
return res.status(200).json({ success: true, printerjob: job });
} catch (error) { } catch (error) {
console.error('Print article label failed:', error); console.error('Print article label failed:', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });

View File

@@ -1,4 +1,5 @@
import { emit } from '../../sse.js'; import { emit } from '../../sse.js';
import * as articleDb from '../../../db/article.js';
// #region Helpers // #region Helpers
@@ -117,20 +118,111 @@ export function articleErrorStatus(error) {
return 500; return 500;
} }
const CURRENCY_FORMATTER = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
});
export function formatPrice(amount) {
if (amount == null || !Number.isFinite(Number(amount))) return '';
return CURRENCY_FORMATTER.format(Number(amount));
}
const BASEPRICE_FORMATTER = new Intl.NumberFormat('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
/** Formats the price per base unit, e.g. "€ 4,05 / L". Empty when unit data or price is missing. */
export function formatBasePrice(grossRaw, article) {
const unit = article?.unit;
const unitamount = Number(article?.unitamount);
const factor = Number(unit?.basesifactor);
if (grossRaw == null || !unit?.basesi || !(unitamount > 0) || !(factor > 0)) return '';
const baseAmount = unitamount * factor;
// Price per base unit equals the price itself: nothing to add.
if (Math.abs(baseAmount - 1) < 1e-9) return '';
const perBase = grossRaw / baseAmount;
const label = factor === 1 && unit.symbol ? unit.symbol : unit.basesi;
return `€ ${BASEPRICE_FORMATTER.format(perBase)} / ${label}`;
}
function grossOf(price) {
const net = Number(price?.net);
if (price?.net == null || !Number.isFinite(net)) return null;
const tax = Number(price.taxRate);
return net * (1 + (Number.isFinite(tax) ? tax : 0) / 100);
}
/**
* Returns the article with `mandatedOptionsGross` set to the summed gross price
* (price × count) of its mandated options, for label placeholders.
*/
export async function withMandatedOptionsPrice(userId, article) {
const mandated = Array.isArray(article?.options) ? article.options.filter((o) => o.mandated) : [];
if (mandated.length === 0) return article;
let extra = 0;
for (const opt of mandated) {
const result = await articleDb.getBySku(userId, opt.sku);
const gross = grossOf(result?.article?.prices?.[0]);
if (gross != null) extra += gross * (Number(opt.count) || 1);
}
return { ...article, mandatedOptionsGross: extra };
}
export function interpolateString(str, article) { export function interpolateString(str, article) {
if (typeof str !== 'string') return str; if (typeof str !== 'string') return str;
const net = article.net_price ?? article.prices?.[0]?.net; const basePrice = Array.isArray(article?.prices) && article.prices.length > 0
const taxRate = article.prices?.[0]?.taxRate ?? 0; ? article.prices[0]
const gross = net != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null; : null;
const ean = (article.eans && article.eans.length > 0) ? article.eans[0] : ''; const net = basePrice?.net != null && Number.isFinite(Number(basePrice.net))
? Number(basePrice.net)
: null;
const taxRate = basePrice?.taxRate != null && Number.isFinite(Number(basePrice.taxRate))
? Number(basePrice.taxRate)
: 0;
const extra = Number(article?.mandatedOptionsGross) || 0;
const grossRaw = net != null ? net * (1 + taxRate / 100) + extra : null;
const gross = grossRaw != null
? Math.round((grossRaw + Number.EPSILON) * 100) / 100
: null;
return str return str
.replace(/\{\{(?:ARTICLE_NAME|article_name|name)\}\}/gi, article.name || '') .replace(/\{\{name\}\}/gi, article?.name || '')
.replace(/\{\{SKU\}\}/gi, article.sku || '') .replace(/\{\{baseprice\}\}/gi, formatBasePrice(grossRaw, article))
.replace(/\{\{(?:NET_PRICE|net_price)\}\}/gi, net != null ? net.toString() : '') .replace(/\{\{price\}\}/gi, formatPrice(gross));
.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); export function interpolateObjects(objects, article) {
if (!Array.isArray(objects)) return [];
const ean = Array.isArray(article?.eans) && article.eans.length > 0 ? String(article.eans[0]).trim() : '';
const sku = String(article?.sku ?? '').trim();
return objects.map((obj) => {
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (cloned.type === 'barcode' || typeof cloned.code === 'string') {
const rawCode = String(cloned.code ?? '').trim();
if (rawCode === '0' || /^0+$/.test(rawCode)) {
if (ean) {
cloned.code = ean;
if (ean.length === 8) {
cloned.format = 'EAN8';
} else if (ean.length === 12 || ean.length === 13) {
cloned.format = 'EAN13';
}
} else if (sku) {
cloned.code = sku;
cloned.format = 'CODE128';
}
} else if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
}
return cloned;
});
} }
// #endregion // #endregion

View File

@@ -253,7 +253,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku); const parsed = parseSku(req.params.sku);
if (parsed.error) return res.status(400).json({ error: parsed.error }); if (parsed.error) return res.status(400).json({ error: parsed.error });
if (req.body?.sku || req.body?.childSku) { if (req.body?.sku) {
return res.status(400).json({ return res.status(400).json({
error: 'sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article', error: 'sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article',
}); });
@@ -282,9 +282,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku); const parsed = parseSku(req.params.sku);
if (parsed.error) return res.status(400).json({ error: parsed.error }); if (parsed.error) return res.status(400).json({ error: parsed.error });
const childSku = typeof req.body?.sku === 'string' && req.body.sku.trim() const childSku = typeof req.body?.sku === 'string' ? req.body.sku.trim() : '';
? req.body.sku.trim()
: (typeof req.body?.childSku === 'string' && req.body.childSku.trim() ? req.body.childSku.trim() : '');
if (!childSku) { if (!childSku) {
return res.status(400).json({ error: 'sku is required' }); return res.status(400).json({ error: 'sku is required' });
} }
@@ -354,7 +352,7 @@ export function register(app, basePath = '') {
const parsedChild = parseSku(req.params.childSku); const parsedChild = parseSku(req.params.childSku);
if (parsedChild.error) return res.status(400).json({ error: 'childSku is required' }); if (parsedChild.error) return res.status(400).json({ error: 'childSku is required' });
const deleteArticle = req.query.deleteArticle === 'true' || req.body?.deleteArticle === true; const deleteArticle = req.query.deleteArticle === 'true';
try { try {
const result = await db.removeChild(req.user.id, parsed.sku, parsedChild.sku, { const result = await db.removeChild(req.user.id, parsed.sku, parsedChild.sku, {

View File

@@ -141,7 +141,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error }); return res.status(400).json({ error: skuParsed.error });
} }
const propertiesInput = req.body?.properties ?? req.body; const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) { if (!Array.isArray(propertiesInput)) {
return res.status(400).json({ error: 'properties array required' }); return res.status(400).json({ error: 'properties array required' });
} }

View File

@@ -2,6 +2,8 @@ import jwt from 'jsonwebtoken';
import { OAuth2Client } from 'google-auth-library'; import { OAuth2Client } from 'google-auth-library';
import env from '../../../config/env.js'; import env from '../../../config/env.js';
import * as db from '../../../db/googleAuth.js'; import * as db from '../../../db/googleAuth.js';
import * as autoinviteDb from '../../../db/autoinvite.js';
import * as companyDb from '../../../db/company.js';
import { clearCachedUser } from '../../middleware/auth.js'; import { clearCachedUser } from '../../middleware/auth.js';
import { downloadPicture, toPictureBuffer } from '../../../util/pictureBinary.js'; import { downloadPicture, toPictureBuffer } from '../../../util/pictureBinary.js';
@@ -20,7 +22,8 @@ export function register(app, basePath = '') {
* tags: [Auth] * tags: [Auth]
* summary: Login with Google ID token * summary: Login with Google ID token
* description: | * description: |
* Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), * Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`
* or when the email has a pending auto-invitation),
* stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie. * stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie.
* `user.picture` is the profile image as base64 (same as GET /auth/me). * `user.picture` is the profile image as base64 (same as GET /auth/me).
* security: [] * security: []
@@ -64,7 +67,7 @@ export function register(app, basePath = '') {
* schema: * schema:
* $ref: '#/components/schemas/Error' * $ref: '#/components/schemas/Error'
* 403: * 403:
* description: Email not on allowlist * description: Email not on allowlist or invited
* content: * content:
* application/json: * application/json:
* schema: * schema:
@@ -104,9 +107,16 @@ export function register(app, basePath = '') {
} }
let allowed = await db.getAllowedEmail(email); let allowed = await db.getAllowedEmail(email);
if (!allowed && env.implicitCreate) { if (!allowed && (env.implicitCreate || (await autoinviteDb.hasPendingInvite(email)))) {
await db.addAllowedEmail(email); await db.addAllowedEmail(email);
allowed = await db.getAllowedEmail(email); allowed = await db.getAllowedEmail(email);
if (allowed && allowed.selectedCompanyId == null) {
const companies = await companyDb.listCompanies(allowed.id);
if (companies.length > 0) {
await db.setSelectedCompany(allowed.id, companies[0].id);
allowed = await db.getAllowedEmail(email);
}
}
} }
if (!allowed) { if (!allowed) {
console.warn(`Login rejected: email not authorized (${email})`); console.warn(`Login rejected: email not authorized (${email})`);

View File

@@ -53,6 +53,12 @@ export function register(app, basePath = '') {
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Error' * $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (the user is not a member of this company)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500: * 500:
* description: Server error * description: Server error
* content: * content:
@@ -79,6 +85,9 @@ export function register(app, basePath = '') {
res.status(200).json({ user }); res.status(200).json({ user });
} catch (error) { } catch (error) {
if (error.message === 'Not authorized') {
return res.status(403).json({ error: error.message });
}
console.error('Set selected company failed:', error); console.error('Set selected company failed:', error);
res.status(500).json({ error: 'Failed to set selected company' }); res.status(500).json({ error: 'Failed to set selected company' });
} }

View File

@@ -0,0 +1,271 @@
import * as db from '../../db/autoinvite.js';
import { emit } from '../sse.js';
// #region Helpers
function requireSelectedCompany(req) {
const companyId = req.user?.selectedCompanyId;
if (companyId == null) {
return { error: 'No company selected' };
}
return { companyId };
}
function parsePositiveInt(val, field = 'id') {
const id = Number(val);
if (!Number.isInteger(id) || id <= 0) {
return { error: `${field} is required` };
}
return { id };
}
function autoinviteErrorStatus(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 === 'Invitation not found') return 404;
if (error.message === 'Invalid role') return 400;
if (error.message === 'Invalid email') return 400;
if (error.message === 'User already registered') return 409;
if (error.message === 'Invitation already exists') return 409;
return 500;
}
// #endregion
/**
* Registers auto-invitation routes. All routes require the caller to be an
* admin of the selected company.
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @openapi
* /autoinvite:
* get:
* tags: [AutoInvite]
* summary: List auto-invitations
* description: Lists pending auto-invitations of the selected company. Admin only.
* security:
* - cookieAuth: []
* x-markdown: autoinvite/list.md
* responses:
* 200:
* description: Pending invitations
* content:
* application/json:
* schema:
* type: object
* properties:
* autoinvites:
* type: array
* items:
* $ref: '#/components/schemas/AutoInvite'
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /autoinvite
app.get(`${basePath}/autoinvite`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const autoinvites = await db.list(req.user.id);
res.status(200).json({ autoinvites });
} catch (error) {
const status = autoinviteErrorStatus(error);
if (status === 500) console.error('List auto-invitations failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /autoinvite:
* post:
* tags: [AutoInvite]
* summary: Create auto-invitation
* description: |
* Registers an email for automatic membership. When a user with this email registers,
* the invitation is removed from the list and the user is added to the company with the given role.
* Emails that are already registered are rejected (409); use `POST /companyuser` for those. Admin only.
* security:
* - cookieAuth: []
* x-signals: [autoinvite]
* x-markdown: autoinvite/create.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [email]
* properties:
* email:
* type: string
* role:
* type: string
* enum: [admin, user]
* default: user
* example:
* email: newhire@example.com
* role: user
* responses:
* 201:
* description: Created invitation
* content:
* application/json:
* schema:
* type: object
* properties:
* autoinvite:
* type: object
* properties:
* id:
* type: integer
* email:
* type: string
* role:
* type: string
* enum: [admin, user]
* 400:
* description: No company selected, invalid email or invalid role
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: User already registered or invitation already exists
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /autoinvite
app.post(`${basePath}/autoinvite`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const autoinvite = await db.add(req.user.id, {
email: req.body?.email,
role: req.body?.role,
});
emit('autoinvite', { companyId: selected.companyId });
res.status(201).json({ autoinvite });
} catch (error) {
const status = autoinviteErrorStatus(error);
if (status === 500) console.error('Create auto-invitation failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /autoinvite/{id}:
* delete:
* tags: [AutoInvite]
* summary: Delete auto-invitation
* description: Deletes a pending auto-invitation of the selected company. Admin only.
* security:
* - cookieAuth: []
* x-signals: [autoinvite]
* x-markdown: autoinvite/delete.md
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 204:
* description: 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'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Invitation not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /autoinvite/:id
app.delete(`${basePath}/autoinvite/: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);
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
try {
await db.remove(req.user.id, parsedId.id);
emit('autoinvite', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = autoinviteErrorStatus(error);
if (status === 500) console.error('Delete auto-invitation failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -252,12 +252,12 @@ export function register(app, basePath = '') {
// #region POST /backup/:id/vfs/recover // #region POST /backup/:id/vfs/recover
// MARK: handles POST /backup/:id/vfs/recover to restore a VFS file from a backup // MARK: handles POST /backup/:id/vfs/recover to restore a VFS file from a backup
const handleRecover = async (req, res) => { const handleRecover = async (req, res) => {
const filePath = req.params.path || req.body?.path || req.body?.file || req.body?.filename || req.query?.path; const filePath = req.body?.path;
if (!filePath) { if (!filePath) {
return res.status(400).json({ error: 'File path is required' }); return res.status(400).json({ error: 'File path is required' });
} }
const requestedCompanyId = req.body?.companyId ?? req.query?.companyId; const requestedCompanyId = req.body?.companyId;
const authorized = await resolveAuthorizedCompany(req, requestedCompanyId); const authorized = await resolveAuthorizedCompany(req, requestedCompanyId);
if (authorized.error) { if (authorized.error) {
return res.status(authorized.status).json({ error: authorized.error }); return res.status(authorized.status).json({ error: authorized.error });

View File

@@ -384,8 +384,6 @@ export function register(app, basePath = '') {
let includeChildren = true; let includeChildren = true;
if (req.query.includeChildren !== undefined) { if (req.query.includeChildren !== undefined) {
includeChildren = req.query.includeChildren !== 'false' && req.query.includeChildren !== '0'; includeChildren = req.query.includeChildren !== 'false' && req.query.includeChildren !== '0';
} else if (req.query.variants !== undefined) {
includeChildren = req.query.variants !== 'false' && req.query.variants !== '0';
} }
try { try {
@@ -395,7 +393,7 @@ export function register(app, basePath = '') {
.map((a) => ({ .map((a) => ({
...a, ...a,
childCount: Number(a.childCount ?? 0), childCount: Number(a.childCount ?? 0),
isParent: Boolean(a.isParent ?? a.is_parent), isParent: Boolean(a.isParent),
smallThumbW: a.smallThumbW ?? null, smallThumbW: a.smallThumbW ?? null,
smallThumbH: a.smallThumbH ?? null, smallThumbH: a.smallThumbH ?? null,
smallThumbBinary: a.smallThumbBinary ?? null, smallThumbBinary: a.smallThumbBinary ?? null,

View File

@@ -153,7 +153,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const propertiesInput = req.body?.properties ?? req.body; const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) { if (!Array.isArray(propertiesInput)) {
return res.status(400).json({ error: 'properties array required' }); return res.status(400).json({ error: 'properties array required' });
} }

View File

@@ -2,13 +2,17 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import multer from 'multer'; import multer from 'multer';
import JSZip from 'jszip'; import JSZip from 'jszip';
import env from '../../config/env.js';
import * as db from '../../db/company.js'; import * as db from '../../db/company.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import * as googleAuth from '../../db/googleAuth.js'; import * as googleAuth from '../../db/googleAuth.js';
import { clearCachedUser } from '../middleware/auth.js'; import { clearCachedUser } from '../middleware/auth.js';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { initCompanyVfs } from '../../tools/vfs.js'; import { initCompanyVfs } from '../../tools/vfs.js';
import { createDefaultLabelTemplates } from '../../services/defaultLabelTemplates.js';
import { executeCompanyImport } from '../../services/companyImport.js'; import { executeCompanyImport } from '../../services/companyImport.js';
import { executeCompanyExport } from '../../services/companyExport.js'; import { executeCompanyExport } from '../../services/companyExport.js';
import { clearCompanyPrinters } from '../../services/companyPrinterService.js';
// #region Helpers // #region Helpers
@@ -85,7 +89,7 @@ function companyErrorStatus(error) {
if (error.message === 'Company not found') return 404; if (error.message === 'Company not found') return 404;
if (error.message === 'Company name not available') return 409; if (error.message === 'Company name not available') return 409;
if (error.message === 'Invalid tax rate' || error.message === 'Text required' || error.message === 'No company selected') return 400; if (error.message === 'Invalid tax rate' || error.message === 'Text required' || error.message === 'No company selected') return 400;
if (error.message === 'Not authorized') return 403; if (error.message === 'Not authorized' || error.message === 'Invited users cannot create a company') return 403;
return 500; return 500;
} }
@@ -184,6 +188,12 @@ export function register(app, basePath = '') {
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Error' * $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (invited users cannot create companies when implicit create is disabled)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/ */
// #region GET /company // #region GET /company
@@ -207,6 +217,13 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: body.error }); return res.status(400).json({ error: body.error });
} }
if (!env.implicitCreate) {
const isInvited = await db.isInvitedUser(req.user.id);
if (isInvited) {
return res.status(403).json({ error: 'Invited users cannot create a company' });
}
}
try { try {
const company = await db.createCompany(req.user.id, body); const company = await db.createCompany(req.user.id, body);
await googleAuth.setSelectedCompany(req.user.id, company.id); await googleAuth.setSelectedCompany(req.user.id, company.id);
@@ -216,6 +233,11 @@ export function register(app, basePath = '') {
} catch (vfsErr) { } catch (vfsErr) {
console.error('Failed to initialize company VFS:', vfsErr); console.error('Failed to initialize company VFS:', vfsErr);
} }
try {
await createDefaultLabelTemplates(req.user.id);
} catch (tplErr) {
console.error('Failed to create default label templates:', tplErr);
}
const authRow = await googleAuth.getAllowedEmail(req.user.email); const authRow = await googleAuth.getAllowedEmail(req.user.email);
res.status(201).json({ company, user: userPayload(authRow) }); res.status(201).json({ company, user: userPayload(authRow) });
} catch (error) { } catch (error) {
@@ -478,13 +500,25 @@ export function register(app, basePath = '') {
app.post( app.post(
`${basePath}/company/import`, `${basePath}/company/import`,
async (req, res, next) => {
try {
const companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
next();
} catch (error) {
const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
}
},
uploadCompanyImportFile, uploadCompanyImportFile,
async (req, res) => { async (req, res) => {
let companyId; let companyId;
try { try {
companyId = requireSelectedCompany(req); companyId = requireSelectedCompany(req);
await requireCompanyAdmin(req.user.id, companyId);
} catch (error) { } catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message }); const status = companyErrorStatus(error);
return res.status(status).json({ error: error.message });
} }
if (activeImportsByCompany.has(companyId)) { if (activeImportsByCompany.has(companyId)) {
@@ -947,7 +981,7 @@ export function register(app, basePath = '') {
} }
try { try {
const company = await db.updateCompany(parsedId.companyId, body); const company = await db.updateCompany(parsedId.companyId, body, req.user.id);
emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } }); emit('company', { companyId: parsedId.companyId, data: { companyId: parsedId.companyId } });
res.status(200).json({ company }); res.status(200).json({ company });
} catch (error) { } catch (error) {
@@ -968,9 +1002,14 @@ export function register(app, basePath = '') {
} }
try { try {
await db.deleteCompany(parsedId.companyId); await db.deleteCompany(parsedId.companyId, req.user.id);
clearCompanyPrinters(parsedId.companyId);
res.sendStatus(204); res.sendStatus(204);
} catch (error) { } catch (error) {
const status = companyErrorStatus(error);
if (status !== 500) {
return res.status(status).json({ error: error.message });
}
console.error('Delete company failed:', error); console.error('Delete company failed:', error);
res.status(500).json({ error: 'Failed to delete company' }); res.status(500).json({ error: 'Failed to delete company' });
} }

View File

@@ -0,0 +1,377 @@
import * as db from '../../db/companyuser.js';
import { clearCachedUser } from '../middleware/auth.js';
import { emit } from '../sse.js';
// #region Helpers
function requireSelectedCompany(req) {
const companyId = req.user?.selectedCompanyId;
if (companyId == null) {
return { error: 'No company selected' };
}
return { companyId };
}
function parsePositiveInt(val, field = 'id') {
const id = Number(val);
if (!Number.isInteger(id) || id <= 0) {
return { error: `${field} is required` };
}
return { id };
}
function companyuserErrorStatus(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 === 'Company user not found') return 404;
if (error.message === 'User not found') return 404;
if (error.message === 'Invalid role') return 400;
if (error.message === 'Invalid email') return 400;
if (error.message === 'User already in company') return 409;
if (error.message === 'Cannot demote the last admin') return 409;
if (error.message === 'Cannot remove the last admin') return 409;
return 500;
}
// #endregion
/**
* Registers company user (membership) routes. All routes require the caller
* to be an admin of the selected company.
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @openapi
* /companyuser:
* get:
* tags: [CompanyUser]
* summary: List company users
* description: Lists the members of the selected company with their role. Admin only.
* security:
* - cookieAuth: []
* x-markdown: companyuser/list.md
* responses:
* 200:
* description: Company members
* content:
* application/json:
* schema:
* type: object
* properties:
* companyusers:
* type: array
* items:
* $ref: '#/components/schemas/CompanyUser'
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /companyuser
app.get(`${basePath}/companyuser`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const companyusers = await db.list(req.user.id);
res.status(200).json({ companyusers });
} catch (error) {
const status = companyuserErrorStatus(error);
if (status === 500) console.error('List company users failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /companyuser:
* post:
* tags: [CompanyUser]
* summary: Add an existing user to the company
* description: |
* Adds an existing (registered) user, identified by email, to the selected company. Admin only.
* The added user can then switch to this company via `PUT /auth/selected-company`.
* For emails that are not registered yet use `POST /autoinvite`.
* security:
* - cookieAuth: []
* x-signals: [companyuser]
* x-markdown: companyuser/create.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [email]
* properties:
* email:
* type: string
* role:
* type: string
* enum: [admin, user]
* default: user
* example:
* email: colleague@example.com
* role: user
* responses:
* 201:
* description: Created company user
* content:
* application/json:
* schema:
* type: object
* properties:
* companyuser:
* $ref: '#/components/schemas/CompanyUser'
* 400:
* description: No company selected, invalid email or invalid role
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: No registered user with this email
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: User already in company
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /companyuser
app.post(`${basePath}/companyuser`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const companyuser = await db.addByEmail(req.user.id, {
email: req.body?.email,
role: req.body?.role,
});
emit('companyuser', { companyId: selected.companyId });
res.status(201).json({ companyuser });
} catch (error) {
const status = companyuserErrorStatus(error);
if (status === 500) console.error('Add company user failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /companyuser/{id}:
* patch:
* tags: [CompanyUser]
* summary: Change a company user's role
* description: Changes the role of a member of the selected company. The last admin cannot be demoted. Admin only.
* security:
* - cookieAuth: []
* x-signals: [companyuser]
* x-markdown: companyuser/update.md
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [role]
* properties:
* role:
* type: string
* enum: [admin, user]
* responses:
* 200:
* description: Updated company user
* content:
* application/json:
* schema:
* type: object
* properties:
* companyuser:
* $ref: '#/components/schemas/CompanyUser'
* 400:
* description: Invalid id, role or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Company user not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Cannot demote the last admin
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region PATCH /companyuser/:id
app.patch(`${basePath}/companyuser/: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);
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
try {
const companyuser = await db.setRole(req.user.id, parsedId.id, req.body?.role);
clearCachedUser(companyuser.googleAuthId);
emit('companyuser', { companyId: selected.companyId });
res.status(200).json({ companyuser });
} catch (error) {
const status = companyuserErrorStatus(error);
if (status === 500) console.error('Update company user failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /companyuser/{id}:
* delete:
* tags: [CompanyUser]
* summary: Remove a user from the company
* description: |
* Removes a member from the selected company. History entries stay but lose their author link;
* if the removed user had this company selected, their selection is cleared.
* The last admin cannot be removed. Admin only.
* security:
* - cookieAuth: []
* x-signals: [companyuser]
* x-markdown: companyuser/delete.md
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 204:
* description: Removed
* 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'
* 403:
* description: Not authorized (admin of the selected company required)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Company user not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 409:
* description: Cannot remove the last admin
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /companyuser/:id
app.delete(`${basePath}/companyuser/: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);
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
try {
const removed = await db.remove(req.user.id, parsedId.id);
clearCachedUser(removed.googleAuthId);
emit('companyuser', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = companyuserErrorStatus(error);
if (status === 500) console.error('Remove company user failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -55,6 +55,10 @@ export function register(app, basePath = '') {
* | `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` | * | `propertyname` | `{}` | `POST /propertyname`, `PUT /propertyname/order`, `PATCH /propertyname/{id}`, `DELETE /propertyname/{id}` |
* | `labeltemplate` | `{}` | `POST /labeltemplate`, `PUT /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `DELETE /labeltemplate/{id}` | * | `labeltemplate` | `{}` | `POST /labeltemplate`, `PUT /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `DELETE /labeltemplate/{id}` |
* | `printer` | `{}` | `POST /printer`, `PUT /printer/{id}`, `PATCH /printer/{id}`, `DELETE /printer/{id}` | * | `printer` | `{}` | `POST /printer`, `PUT /printer/{id}`, `PATCH /printer/{id}`, `DELETE /printer/{id}` |
* | `printeragent` | `{}` | `PUT /printeragent`, `PATCH /printeragent/{id}`, `DELETE /printeragent/{id}`, printer agent connect/disconnect/state |
* | `agentprinter` | `{ printers }` or `{}` | `PATCH /agentprinter/{id}`, `PUT /agentprinter/order`, `DELETE /agentprinter/{id}`, agent state updates / printer discovery |
* | `companyuser` | `{}` | `POST /companyuser`, `PATCH /companyuser/{id}`, `DELETE /companyuser/{id}` |
* | `autoinvite` | `{}` | `POST /autoinvite`, `DELETE /autoinvite/{id}` |
* | `warehouse` | `{}` | `POST /warehouse`, `PUT /warehouse/{id}`, `PATCH /warehouse/{id}`, `DELETE /warehouse/{id}` | * | `warehouse` | `{}` | `POST /warehouse`, `PUT /warehouse/{id}`, `PATCH /warehouse/{id}`, `DELETE /warehouse/{id}` |
* | `supplier` | `{}` | `POST /supplier`, `PATCH /supplier/{id}`, `DELETE /supplier/{id}` | * | `supplier` | `{}` | `POST /supplier`, `PATCH /supplier/{id}`, `DELETE /supplier/{id}` |
* | `vfs` | `{}` | VFS uploads and filesystem mutations such as `POST /vfs/upload`, `POST /vfs/mkdir`, `POST /vfs/rename`, `PUT /vfs/{*path}`, `DELETE /vfs/{*path}` | * | `vfs` | `{}` | VFS uploads and filesystem mutations such as `POST /vfs/upload`, `POST /vfs/mkdir`, `POST /vfs/rename`, `PUT /vfs/{*path}`, `DELETE /vfs/{*path}` |
@@ -93,10 +97,20 @@ export function register(app, basePath = '') {
* payload: "{}" * payload: "{}"
* - name: propertyname * - name: propertyname
* payload: "{}" * payload: "{}"
* - name: companyuser
* payload: "{}"
* - name: autoinvite
* payload: "{}"
* - name: labeltemplate * - name: labeltemplate
* payload: "{}" * payload: "{}"
* - name: printer * - name: printer
* payload: "{}" * payload: "{}"
* - name: printeragent
* payload: "{}"
* - name: agentprinter
* payload: "{ printers } or {}"
* - name: printerjob
* payload: "{ job } or { jobId, state }"
* - name: warehouse * - name: warehouse
* payload: "{}" * payload: "{}"
* - name: supplier * - name: supplier

View File

@@ -1,7 +1,14 @@
import { validateObjects } from 'react-canvas-label-editor'; import { renderLabel, validateObjects } from 'react-canvas-label-editor';
import * as db from '../../db/labeltemplate.js'; import * as db from '../../db/labeltemplate.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, interpolateObjects, withMandatedOptionsPrice } from './article/shared.js';
// #region Helpers // #region Helpers
@@ -89,10 +96,45 @@ export function register(app, basePath = '') {
try { try {
const templates = await db.list(req.user.id); const templates = await db.list(req.user.id);
res.status(200).json({ labeltemplates: templates.map((t) => {
let article = null;
if (req.query?.sku) {
const skuStr = String(req.query.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (result?.article) {
article = await withMandatedOptionsPrice(req.user.id, result.article);
}
}
const labeltemplates = await Promise.all(
templates.map(async (t) => {
const { content, ...rest } = serializeTemplate(t); const { content, ...rest } = serializeTemplate(t);
if (article) {
try {
const full = await db.getById(req.user.id, t.id);
const objects = Array.isArray(full?.content?.objects)
? full.content.objects
: Array.isArray(full?.content)
? full.content
: null;
if (objects && objects.length > 0) {
const interpolated = interpolateObjects(objects, article);
const thumbBuffer = await renderThumb({
width: t.width,
height: t.height,
objects: interpolated,
});
rest.thumb = thumbBuffer.toString('base64');
}
} catch (err) {
console.error(`Failed to render preview thumb for template ${t.id} with sku ${req.query.sku}:`, err);
}
}
return rest; return rest;
}) }); })
);
res.status(200).json({ labeltemplates });
} catch (error) { } catch (error) {
const status = labeltemplateErrorStatus(error); const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('List label templates failed:', error); if (status === 500) console.error('List label templates failed:', error);
@@ -166,7 +208,32 @@ export function register(app, basePath = '') {
if (!template) { if (!template) {
return res.status(404).json({ error: 'Label template not found' }); return res.status(404).json({ error: 'Label template not found' });
} }
res.status(200).json({ labeltemplate: serializeTemplate(template) }); const serialized = serializeTemplate(template);
if (req.query?.sku) {
const skuStr = String(req.query.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (result?.article) {
const objects = Array.isArray(serialized?.content?.objects)
? serialized.content.objects
: Array.isArray(serialized?.content)
? serialized.content
: null;
if (objects && objects.length > 0) {
try {
const interpolated = interpolateObjects(objects, await withMandatedOptionsPrice(req.user.id, result.article));
const thumbBuffer = await renderThumb({
width: template.width,
height: template.height,
objects: interpolated,
});
serialized.thumb = thumbBuffer.toString('base64');
} catch (err) {
console.error(`Failed to render preview thumb for template ${template.id} with sku ${req.query.sku}:`, err);
}
}
}
}
res.status(200).json({ labeltemplate: serialized });
} catch (error) { } catch (error) {
const status = labeltemplateErrorStatus(error); const status = labeltemplateErrorStatus(error);
if (status === 500) console.error('Get label template failed:', error); if (status === 500) console.error('Get label template failed:', error);
@@ -482,6 +549,173 @@ 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
* required: [agentprinterId]
* properties:
* agentprinterId:
* type: integer
* description: Agent printer ID
* 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 parsedPrinterId = parsePositiveInt(req.body?.agentprinterId, 'agentprinterId');
if (parsedPrinterId.error) {
return res.status(400).json({ error: parsedPrinterId.error });
}
try {
const printer = await agentprinterDb.getById(req.user.id, parsedPrinterId.id);
if (!printer) {
return res.status(404).json({ error: 'Printer not found' });
}
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) {
const skuStr = String(req.body.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (!result?.article) {
return res.status(404).json({ error: 'Article not found' });
}
article = await withMandatedOptionsPrice(req.user.id, result.article);
} else {
article = {
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 = interpolateObjects(objects, article);
const printPayload = await renderLabel({
width: width || 448,
height: height || 200,
objects: interpolatedObjects,
});
zplData = await pngToZpl(printPayload, quantity);
}
const job = await printerjobDb.create(req.user.id, {
agentprinterId: printer.id,
type: 'zpl',
data: zplData,
});
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'created', job);
}
return res.status(200).json({ success: true, printerjob: job });
} 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
} }
/** /**

View File

@@ -101,7 +101,7 @@ export function register(app, basePath = '') {
} }
const manifestarticleId = Number(req.params.manifestarticleId); const manifestarticleId = Number(req.params.manifestarticleId);
const transactionType = req.query.transaction_type ?? req.query.transactionType; const transactionType = req.query.transaction_type;
try { try {
const ledger = await db.listByManifestarticleId( const ledger = await db.listByManifestarticleId(
@@ -186,7 +186,7 @@ export function register(app, basePath = '') {
} }
const sku = req.params.sku; const sku = req.params.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType; const transactionType = req.query.transaction_type;
try { try {
const ledger = await db.listBySku(req.user.id, sku, transactionType); const ledger = await db.listBySku(req.user.id, sku, transactionType);
@@ -254,9 +254,9 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const rawManifestarticleId = req.query.manifestarticle_id ?? req.query.manifestarticleId; const rawManifestarticleId = req.query.manifestarticle_id;
const sku = req.query.sku; const sku = req.query.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType; const transactionType = req.query.transaction_type;
if (rawManifestarticleId == null && (sku == null || !String(sku).trim())) { if (rawManifestarticleId == null && (sku == null || !String(sku).trim())) {
return res.status(400).json({ return res.status(400).json({

View File

@@ -190,12 +190,8 @@ export function register(app, basePath = '') {
} }
const pairingpin = String(randomInt(100000, 999999)); const pairingpin = String(randomInt(100000, 999999));
const rootCategoryId = req.body.rootCategoryId !== undefined const rootCategoryId = req.body.rootCategoryId;
? req.body.rootCategoryId const syncRootArticles = req.body.syncRootArticles;
: req.body.root_category_id;
const syncRootArticles = req.body.syncRootArticles !== undefined
? req.body.syncRootArticles
: req.body.sync_root_articles;
const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined; const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined;
if (sort !== undefined && !Number.isInteger(sort)) { if (sort !== undefined && !Number.isInteger(sort)) {
return res.status(400).json({ error: 'sort must be an integer' }); return res.status(400).json({ error: 'sort must be an integer' });
@@ -319,14 +315,12 @@ export function register(app, basePath = '') {
payload.name = req.body.name.trim(); payload.name = req.body.name.trim();
} }
if (req.body.rootCategoryId !== undefined || req.body.root_category_id !== undefined) { if (req.body.rootCategoryId !== undefined) {
const rawCat = req.body.rootCategoryId !== undefined ? req.body.rootCategoryId : req.body.root_category_id; payload.rootCategoryId = req.body.rootCategoryId != null ? Number(req.body.rootCategoryId) : null;
payload.rootCategoryId = rawCat != null ? Number(rawCat) : null;
} }
if (req.body.syncRootArticles !== undefined || req.body.sync_root_articles !== undefined) { if (req.body.syncRootArticles !== undefined) {
const rawSync = req.body.syncRootArticles !== undefined ? req.body.syncRootArticles : req.body.sync_root_articles; payload.syncRootArticles = Boolean(req.body.syncRootArticles);
payload.syncRootArticles = Boolean(rawSync);
} }
if (req.body.sort !== undefined) { if (req.body.sort !== undefined) {

View File

@@ -1,5 +1,6 @@
import * as db from '../../db/printer.js'; import * as db from '../../db/printer.js';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { getCompanyPrinters, getCompanyPrinter } from '../../services/companyPrinterService.js';
// #region Helpers // #region Helpers
@@ -90,7 +91,17 @@ export function register(app, basePath = '') {
try { try {
const printers = await db.list(req.user.id); const printers = await db.list(req.user.id);
res.status(200).json({ printers }); const livePrinters = getCompanyPrinters(selected.companyId);
const enriched = printers.map((p) => {
const live = livePrinters.find((lp) =>
(p.serial && lp.serial && String(p.serial).toLowerCase() === String(lp.serial).toLowerCase()) ||
(p.address && lp.ip && String(p.address).toLowerCase() === String(lp.ip).toLowerCase()) ||
(p.address && lp.address && String(p.address).toLowerCase() === String(lp.address).toLowerCase()) ||
(p.name && lp.name && String(p.name).toLowerCase() === String(lp.name).toLowerCase())
);
return live ? { ...p, state: live } : p;
});
res.status(200).json({ printers: enriched });
} catch (error) { } catch (error) {
const status = printerErrorStatus(error); const status = printerErrorStatus(error);
if (status === 500) console.error('List printers failed:', error); if (status === 500) console.error('List printers failed:', error);
@@ -100,6 +111,186 @@ export function register(app, basePath = '') {
// #endregion // #endregion
/**
* @openapi
* /printer/state:
* get:
* tags: [Printer]
* summary: Get live printer states
* description: |
* Retrieves real-time operational status and telemetry for all discovered printers
* reported by active printer agents for the authenticated user's selected company.
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: serial
* schema:
* type: string
* description: Optional serial number filter
* - in: query
* name: agentId
* schema:
* type: integer
* description: Optional agent ID filter
* responses:
* 200:
* description: Live printer states
* content:
* application/json:
* schema:
* type: object
* properties:
* printers:
* type: array
* items:
* type: object
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /printer/state
app.get(`${basePath}/printer/state`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
let printers = getCompanyPrinters(selected.companyId);
if (req.query?.serial) {
const serialFilter = String(req.query.serial).trim().toLowerCase();
printers = printers.filter((p) => p.serial && String(p.serial).toLowerCase() === serialFilter);
}
if (req.query?.agentId) {
const aId = Number(req.query.agentId);
if (Number.isInteger(aId)) {
printers = printers.filter((p) => p.agentId === aId);
}
}
res.status(200).json({ printers });
});
// #endregion
/**
* @openapi
* /printer/{id}/state:
* get:
* tags: [Printer]
* summary: Get live state for a printer
* description: |
* Retrieves live operational status for a single printer by configured printer ID,
* serial number, or device identifier for the authenticated user's selected company.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* description: Configured printer ID, serial number, or device identifier
* responses:
* 200:
* description: Live printer state
* content:
* application/json:
* schema:
* type: object
* properties:
* printer:
* type: object
* 400:
* description: No company selected or invalid ID
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Printer not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /printer/:id/state
const handlePrinterStateById = async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const identifier = req.params.id;
if (!identifier) {
return res.status(400).json({ error: 'id is required' });
}
let livePrinter = getCompanyPrinter(selected.companyId, identifier);
const numericId = Number(identifier);
if (!livePrinter && Number.isInteger(numericId) && numericId > 0) {
try {
const dbPrinter = await db.getById(req.user.id, numericId);
if (dbPrinter) {
const allLive = getCompanyPrinters(selected.companyId);
livePrinter = allLive.find((lp) =>
(dbPrinter.serial && lp.serial && String(dbPrinter.serial).toLowerCase() === String(lp.serial).toLowerCase()) ||
(dbPrinter.address && lp.ip && String(dbPrinter.address).toLowerCase() === String(lp.ip).toLowerCase()) ||
(dbPrinter.address && lp.address && String(dbPrinter.address).toLowerCase() === String(lp.address).toLowerCase()) ||
(dbPrinter.name && lp.name && String(dbPrinter.name).toLowerCase() === String(lp.name).toLowerCase())
) || null;
if (!livePrinter) {
return res.status(200).json({
printer: {
id: dbPrinter.id,
companyId: dbPrinter.company_id,
serial: dbPrinter.serial,
address: dbPrinter.address,
model: dbPrinter.model,
status: 'offline',
online: false,
},
});
}
}
} catch {
// Ignore DB error, proceed to 404
}
}
if (!livePrinter) {
return res.status(404).json({ error: 'Printer not found' });
}
res.status(200).json({ printer: livePrinter });
};
app.get(`${basePath}/printer/state/:id`, handlePrinterStateById);
app.get(`${basePath}/printer/:id/state`, handlePrinterStateById);
// #endregion
/** /**
* @openapi * @openapi
* /printer/order: * /printer/order:

View File

@@ -1,5 +1,7 @@
import { randomInt } from 'node:crypto'; import { randomInt } from 'node:crypto';
import * as db from '../../db/printeragent.js'; import * as db from '../../db/printeragent.js';
import { emit } from '../sse.js';
import { enrichAgentWithState, removeAgentState } from '../agent/stateStore.js';
// #region Helpers // #region Helpers
@@ -92,7 +94,7 @@ export function register(app, basePath = '') {
} }
try { try {
const printeragents = await db.list(req.user.id); const printeragents = (await db.list(req.user.id)).map(enrichAgentWithState);
res.status(200).json({ printeragents }); res.status(200).json({ printeragents });
} catch (error) { } catch (error) {
const status = printeragentErrorStatus(error); const status = printeragentErrorStatus(error);
@@ -126,6 +128,8 @@ export function register(app, basePath = '') {
* type: string * type: string
* sort: * sort:
* type: integer * type: integer
* enabled:
* type: boolean
* default: 0 * default: 0
* example: * example:
* name: "Bar printer" * name: "Bar printer"
@@ -193,7 +197,8 @@ export function register(app, basePath = '') {
pairingpin, pairingpin,
sort, sort,
}); });
res.status(200).json({ printeragent }); emit('printeragent', { companyId: selected.companyId });
res.status(200).json({ printeragent: enrichAgentWithState(printeragent) });
} catch (error) { } catch (error) {
const status = printeragentErrorStatus(error); const status = printeragentErrorStatus(error);
if (status === 500) console.error('Create printeragent failed:', error); if (status === 500) console.error('Create printeragent failed:', error);
@@ -229,6 +234,8 @@ export function register(app, basePath = '') {
* type: string * type: string
* sort: * sort:
* type: integer * type: integer
* enabled:
* type: boolean
* responses: * responses:
* 200: * 200:
* description: Updated printer agent * description: Updated printer agent
@@ -278,8 +285,8 @@ export function register(app, basePath = '') {
} }
const { name } = req.body ?? {}; const { name } = req.body ?? {};
if (name === undefined && req.body?.sort === undefined) { if (name === undefined && req.body?.sort === undefined && req.body?.enabled === undefined) {
return res.status(400).json({ error: 'name or sort is required' }); return res.status(400).json({ error: 'name, sort, or enabled is required' });
} }
if (name !== undefined && (typeof name !== 'string' || !name.trim())) { if (name !== undefined && (typeof name !== 'string' || !name.trim())) {
@@ -297,10 +304,12 @@ export function register(app, basePath = '') {
const payload = { id: parsed.id }; const payload = { id: parsed.id };
if (name !== undefined) payload.name = name.trim(); if (name !== undefined) payload.name = name.trim();
if (sort !== undefined) payload.sort = sort; if (sort !== undefined) payload.sort = sort;
if (req.body?.enabled !== undefined) payload.enabled = Boolean(req.body.enabled);
try { try {
const printeragent = await db.update(req.user.id, payload); const printeragent = await db.update(req.user.id, payload);
res.status(200).json({ printeragent }); emit('printeragent', { companyId: selected.companyId });
res.status(200).json({ printeragent: enrichAgentWithState(printeragent) });
} catch (error) { } catch (error) {
const status = printeragentErrorStatus(error); const status = printeragentErrorStatus(error);
if (status === 500) console.error('Update printeragent failed:', error); if (status === 500) console.error('Update printeragent failed:', error);
@@ -367,6 +376,8 @@ export function register(app, basePath = '') {
try { try {
await db.remove(req.user.id, parsed.id); await db.remove(req.user.id, parsed.id);
removeAgentState(parsed.id);
emit('printeragent', { companyId: selected.companyId });
res.sendStatus(204); res.sendStatus(204);
} catch (error) { } catch (error) {
const status = printeragentErrorStatus(error); const status = printeragentErrorStatus(error);

View File

@@ -0,0 +1,455 @@
import * as printerjobDb from '../../db/printerjob.js';
import { notifyAgentJob } from '../agent/index.js';
// #region Helpers
function requireSelectedCompany(req) {
const companyId = req.user?.selectedCompanyId;
if (companyId == null) {
return { error: 'No company selected' };
}
return { companyId: Number(companyId) };
}
function parsePositiveInt(value, field) {
const id = Number(value);
if (!Number.isInteger(id) || id <= 0) {
return { error: `${field} must be a positive integer` };
}
return { id };
}
function printerjobErrorStatus(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 === 'Agentprinter not found') return 404;
if (error.message === 'Printerjob not found') return 404;
if (error.message?.includes('is required')) return 400;
if (error.message?.startsWith('Invalid printerjob')) return 400;
return 500;
}
// #endregion
/**
* Registers printerjob routes.
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @openapi
* /printerjob:
* get:
* tags: [PrinterJob]
* summary: List printer jobs
* description: Lists printer jobs for the selected company, optionally filtered by agent printer or state.
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: agentprinterId
* schema:
* type: integer
* description: Optional agent printer ID filter
* - in: query
* name: state
* schema:
* type: string
* enum: [pending, processing, completed, cancelled, failed]
* description: Optional job state filter
* responses:
* 200:
* description: List of printer jobs
* content:
* application/json:
* schema:
* type: object
* properties:
* printerjobs:
* type: array
* items:
* $ref: '#/components/schemas/Printerjob'
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /printerjob
app.get(`${basePath}/printerjob`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const agentprinterId = req.query?.agentprinterId ? Number(req.query.agentprinterId) : undefined;
const state = req.query?.state ? String(req.query.state).trim().toLowerCase() : undefined;
const printerjobs = await printerjobDb.list(req.user.id, { agentprinterId, state });
res.status(200).json({ printerjobs });
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('List printerjobs failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /printerjob/{id}:
* get:
* tags: [PrinterJob]
* summary: Get printer job
* description: Retrieves details of a specific printer job by ID.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Printer job details
* content:
* application/json:
* schema:
* type: object
* properties:
* printerjob:
* $ref: '#/components/schemas/Printerjob'
* 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: Printer job not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /printerjob/:id
app.get(`${basePath}/printerjob/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
}
try {
const printerjob = await printerjobDb.getById(req.user.id, parsed.id);
if (!printerjob) {
return res.status(404).json({ error: 'Printerjob not found' });
}
res.status(200).json({ printerjob });
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('Get printerjob failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /printerjob:
* post:
* tags: [PrinterJob]
* summary: Create print job
* description: Creates a new print job for an agent printer. Notifies the agent in real time.
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [agentprinterId, data]
* properties:
* agentprinterId:
* type: integer
* type:
* type: string
* example: "zpl"
* data:
* type: string
* responses:
* 201:
* description: Printer job created
* content:
* application/json:
* schema:
* type: object
* properties:
* printerjob:
* $ref: '#/components/schemas/Printerjob'
*/
// #region POST /printerjob
app.post(`${basePath}/printerjob`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const rawApId = req.body?.agentprinterId;
const parsedApId = parsePositiveInt(rawApId, 'agentprinterId');
if (parsedApId.error) {
return res.status(400).json({ error: parsedApId.error });
}
const rawData = req.body?.data;
if (!rawData || typeof rawData !== 'string' || !rawData.trim()) {
return res.status(400).json({ error: 'data is required' });
}
const type = typeof req.body?.type === 'string' && req.body.type.trim()
? req.body.type.trim().toLowerCase()
: undefined;
try {
const job = await printerjobDb.create(req.user.id, {
agentprinterId: parsedApId.id,
type,
data: rawData.trim(),
});
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'created', job);
}
res.status(201).json({ printerjob: job });
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('Create printerjob failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /printerjob/cancel-all:
* post:
* tags: [PrinterJob]
* summary: Cancel all pending and processing printer jobs
* description: |
* Cancels all queued or in-flight print jobs for the selected company, optionally filtered by agentprinterId.
* Notifies connected agents in real time.
* security:
* - cookieAuth: []
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* agentprinterId:
* type: integer
* responses:
* 200:
* description: All matching jobs cancelled
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* cancelledCount:
* type: integer
* jobs:
* type: array
* items:
* $ref: '#/components/schemas/Printerjob'
*/
// #region POST /printerjob/cancel-all
app.post(`${basePath}/printerjob/cancel-all`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const rawApId = req.body?.agentprinterId;
const agentprinterId = rawApId != null ? Number(rawApId) : undefined;
const allJobs = await printerjobDb.list(req.user.id, { agentprinterId });
const cancellable = allJobs.filter((j) => j.state === 'pending' || j.state === 'processing');
const cancelledJobs = [];
for (const j of cancellable) {
try {
const job = await printerjobDb.updateState(req.user.id, j.id, 'cancelled');
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'cancelled', job);
}
cancelledJobs.push(job);
} catch (e) {
console.error(`Failed to cancel printerjob #${j.id}:`, e);
}
}
res.status(200).json({
success: true,
cancelledCount: cancelledJobs.length,
jobs: cancelledJobs,
});
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('Cancel all printerjobs failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /printerjob/{id}/cancel:
* post:
* tags: [PrinterJob]
* summary: Cancel printer job
* description: Marks a print job as cancelled and notifies the connected agent in real time.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Printer job cancelled
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* printerjob:
* $ref: '#/components/schemas/Printerjob'
*/
// #region POST /printerjob/:id/cancel
app.post(`${basePath}/printerjob/:id/cancel`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
}
try {
const job = await printerjobDb.updateState(req.user.id, parsed.id, 'cancelled');
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'cancelled', job);
}
res.status(200).json({ success: true, printerjob: job });
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('Cancel printerjob failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /printerjob/{id}:
* delete:
* tags: [PrinterJob]
* summary: Cancel and delete printer job
* description: |
* Cancels and deletes a print job. If the job was pending or processing, sends a cancellation
* event to the connected printer agent before deletion.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Job cancelled and deleted
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* deletedJob:
* $ref: '#/components/schemas/Printerjob'
*/
// #region DELETE /printerjob/:id
app.delete(`${basePath}/printerjob/:id`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsed = parsePositiveInt(req.params.id, 'id');
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
}
try {
const job = await printerjobDb.deleteJob(req.user.id, parsed.id);
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'cancelled', job);
}
res.status(200).json({ success: true, deletedJob: job });
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('Delete printerjob failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -517,7 +517,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const skus = parseSkus(req.query.skus ?? req.query.sku); const skus = parseSkus(req.query.skus);
try { try {
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus); const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);
@@ -540,7 +540,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error }); return res.status(400).json({ error: idParsed.error });
} }
const skus = parseSkus(req.body?.skus ?? req.body?.sku); const skus = parseSkus(req.body?.skus);
try { try {
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus); const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);

View File

@@ -2,10 +2,13 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import multer from 'multer'; import multer from 'multer';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { requireCompanyAdmin } from '../../db/companyuser.js';
import { import {
prepareVfs, prepareVfs,
resolveVfsRoot, resolveVfsRoot,
isReadOnlyDatabasePath, isReadOnlyDatabasePath,
isProtectedAdminVfsPath,
directoryContainsProtectedAdminVfsFile,
getMimeType, getMimeType,
restoreDefaultSkills, restoreDefaultSkills,
restoreDefaultApplets, restoreDefaultApplets,
@@ -586,6 +589,8 @@ export function register(app, basePath = '') {
: ''; : '';
const uploaded = []; const uploaded = [];
const parsedEntries = [];
let touchesProtected = false;
for (const f of files) { for (const f of files) {
const origName = (f.originalname || 'upload').replace(/[\0\\]/g, '/'); const origName = (f.originalname || 'upload').replace(/[\0\\]/g, '/');
const relPath = targetDir ? `${targetDir}/${origName}` : origName; const relPath = targetDir ? `${targetDir}/${origName}` : origName;
@@ -596,6 +601,18 @@ export function register(app, basePath = '') {
const absPath = path.resolve(vfsRoot, parsed.name); const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) continue; if (!absPath.startsWith(vfsRoot)) continue;
if (isProtectedAdminVfsPath(parsed.name)) {
touchesProtected = true;
}
parsedEntries.push({ f, parsed, absPath });
}
if (touchesProtected) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
for (const { f, parsed, absPath } of parsedEntries) {
fs.mkdirSync(path.dirname(absPath), { recursive: true }); fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, f.buffer); fs.writeFileSync(absPath, f.buffer);
@@ -685,7 +702,7 @@ export function register(app, basePath = '') {
if (selected.error) { if (selected.error) {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const raw = req.body?.path || req.body?.dir || req.body?.name; const raw = req.body?.path;
const parsed = parseVfsRef(raw); const parsed = parseVfsRef(raw);
if (parsed.error) { if (parsed.error) {
return res.status(400).json({ error: parsed.error }); return res.status(400).json({ error: parsed.error });
@@ -787,8 +804,8 @@ export function register(app, basePath = '') {
if (selected.error) { if (selected.error) {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const rawOld = req.body?.oldPath || req.body?.from || req.body?.source; const rawOld = req.body?.oldPath;
const rawNew = req.body?.newPath || req.body?.to || req.body?.target; const rawNew = req.body?.newPath;
const parsedOld = parseVfsRef(rawOld); const parsedOld = parseVfsRef(rawOld);
if (parsedOld.error) { if (parsedOld.error) {
return res.status(400).json({ error: `oldPath: ${parsedOld.error}` }); return res.status(400).json({ error: `oldPath: ${parsedOld.error}` });
@@ -815,6 +832,15 @@ export function register(app, basePath = '') {
return res.status(404).json({ error: 'Source file or directory not found' }); return res.status(404).json({ error: 'Source file or directory not found' });
} }
if (
isProtectedAdminVfsPath(parsedOld.name) ||
isProtectedAdminVfsPath(parsedNew.name) ||
directoryContainsProtectedAdminVfsFile(absOld) ||
directoryContainsProtectedAdminVfsFile(absNew)
) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
fs.mkdirSync(path.dirname(absNew), { recursive: true }); fs.mkdirSync(path.dirname(absNew), { recursive: true });
fs.renameSync(absOld, absNew); fs.renameSync(absOld, absNew);
@@ -961,6 +987,9 @@ export function register(app, basePath = '') {
} }
try { try {
if (isProtectedAdminVfsPath(parsed.name)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
const absPath = path.resolve(vfsRoot, parsed.name); const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) { if (!absPath.startsWith(vfsRoot)) {
@@ -1058,6 +1087,9 @@ export function register(app, basePath = '') {
} }
try { try {
if (isProtectedAdminVfsPath(parsed.name)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
const absPath = path.resolve(vfsRoot, parsed.name); const absPath = path.resolve(vfsRoot, parsed.name);
if (!absPath.startsWith(vfsRoot)) { if (!absPath.startsWith(vfsRoot)) {
@@ -1066,6 +1098,9 @@ export function register(app, basePath = '') {
if (!fs.existsSync(absPath)) { if (!fs.existsSync(absPath)) {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
if (directoryContainsProtectedAdminVfsFile(absPath)) {
await requireCompanyAdmin(req.user.id, selected.companyId);
}
const stat = fs.statSync(absPath); const stat = fs.statSync(absPath);
if (stat.isDirectory()) { if (stat.isDirectory()) {
fs.rmSync(absPath, { recursive: true, force: true }); fs.rmSync(absPath, { recursive: true, force: true });

172
src/api/routes/zpljob.js Normal file
View File

@@ -0,0 +1,172 @@
import * as printerjobDb from '../../db/printerjob.js';
import { notifyAgentJob } from '../agent/index.js';
// #region Helpers
function requireSelectedCompany(req) {
const companyId = req.user?.selectedCompanyId;
if (companyId == null) {
return { error: 'No company selected' };
}
return { companyId: Number(companyId) };
}
function parsePositiveInt(value, field) {
const id = Number(value);
if (!Number.isInteger(id) || id <= 0) {
return { error: `${field} must be a positive integer` };
}
return { id };
}
function printerjobErrorStatus(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 === 'Agentprinter not found') return 404;
if (error.message === 'Printerjob not found') return 404;
if (error.message?.includes('is required')) return 400;
if (error.message?.startsWith('Invalid printerjob')) return 400;
return 500;
}
/**
* Adjusts or inserts the ZPL print quantity command (^PQ) in the ZPL payload.
*
* @param {string} data - Raw ZPL string.
* @param {number|string} [copiescount=1] - Number of copies.
* @returns {string}
*/
export function applyZplCopies(data, copiescount) {
if (!data || typeof data !== 'string') return data;
const copies = Number(copiescount);
if (!Number.isInteger(copies) || copies <= 1) return data;
if (/\^PQ\d+/i.test(data)) {
return data.replace(/\^PQ\d+/gi, `^PQ${copies}`);
}
if (/\^XZ/i.test(data)) {
return data.replace(/\^XZ/gi, `^PQ${copies}\n^XZ`);
}
return `${data}\n^PQ${copies}`;
}
// #endregion
/**
* Registers zpljob routes.
* @param {import('express').Application} app
* @param {string} [basePath='']
*/
export function register(app, basePath = '') {
/**
* @openapi
* /zpljob:
* post:
* tags: [PrinterJob]
* summary: Create ZPL print job
* description: |
* Issues a new ZPL print job to an agent printer. If copiescount > 1, the ZPL quantity command (^PQ)
* is automatically applied. The connected printer agent receives an immediate real-time event.
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [agentprinterId, data]
* properties:
* agentprinterId:
* type: integer
* description: Target agent printer ID
* example: 1
* copiescount:
* type: integer
* description: Optional copy count (defaults to 1)
* default: 1
* example: 2
* data:
* type: string
* description: Raw ZPL string
* example: "^XA^FO50,50^ADN,36,20^FDHello Zebra^FS^XZ"
* responses:
* 201:
* description: Print job created
* content:
* application/json:
* schema:
* type: object
* properties:
* printerjob:
* $ref: '#/components/schemas/Printerjob'
* 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'
* 404:
* description: Agent printer not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /zpljob
app.post(`${basePath}/zpljob`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsedApId = parsePositiveInt(req.body?.agentprinterId, 'agentprinterId');
if (parsedApId.error) {
return res.status(400).json({ error: parsedApId.error });
}
const rawData = req.body?.data;
if (!rawData || typeof rawData !== 'string' || !rawData.trim()) {
return res.status(400).json({ error: 'data is required' });
}
const rawCopies = req.body?.copiescount;
const copies = rawCopies != null ? Number(rawCopies) : 1;
if (!Number.isInteger(copies) || copies <= 0) {
return res.status(400).json({ error: 'copiescount must be a positive integer' });
}
const finalData = applyZplCopies(rawData.trim(), copies);
try {
const job = await printerjobDb.create(req.user.id, {
agentprinterId: parsedApId.id,
type: 'zpl',
data: finalData,
});
if (job.printeragentId) {
notifyAgentJob(job.printeragentId, 'created', job);
}
res.status(201).json({ printerjob: job });
} catch (error) {
const status = printerjobErrorStatus(error);
if (status === 500) console.error('Create zpljob failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -87,7 +87,7 @@ export function buildStudioApp() {
// 5. Insert row // 5. Insert row
app.post('/api/tables/:table/row', async (req, res) => { app.post('/api/tables/:table/row', async (req, res) => {
try { try {
const result = await dbHelper.insertRow(req.params.table, req.body.row || req.body); const result = await dbHelper.insertRow(req.params.table, req.body.row);
res.json(result); res.json(result);
} catch (err) { } catch (err) {
res.status(400).json({ error: err.message }); res.status(400).json({ error: err.message });
@@ -175,7 +175,3 @@ export async function stopBackendMonitor() {
}); });
monitorLog.info('Backend Monitor Interface stopped'); monitorLog.info('Backend Monitor Interface stopped');
} }
// Aliases for compatibility
export const startSqlStudio = startBackendMonitor;
export const stopSqlStudio = stopBackendMonitor;

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,11 +7,13 @@ export async function list(googleAuthId) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.account.create(googleAuthId, data); return store.account.create(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.account.delete(googleAuthId, id); return store.account.delete(googleAuthId, id);
} }

36
src/db/agentprinter.js Normal file
View File

@@ -0,0 +1,36 @@
import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId, printeragentId) {
const store = await getStore();
return store.agentprinter.list(googleAuthId, printeragentId);
}
export async function getById(googleAuthId, id) {
const store = await getStore();
return store.agentprinter.getById(googleAuthId, id);
}
export async function findOrCreate(printeragentId, data) {
const store = await getStore();
return store.agentprinter.findOrCreate(printeragentId, data);
}
export async function update(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.agentprinter.update(googleAuthId, data);
}
export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.agentprinter.order(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore();
return store.agentprinter.delete(googleAuthId, id);
}

27
src/db/autoinvite.js Normal file
View File

@@ -0,0 +1,27 @@
import { getStore, getProvider } from './store.js';
export async function list(googleAuthId) {
const store = await getStore();
return store.autoinvite.list(googleAuthId);
}
export async function add(googleAuthId, data) {
const store = await getStore();
return store.autoinvite.add(googleAuthId, data);
}
export async function remove(googleAuthId, id) {
const store = await getStore();
return store.autoinvite.remove(googleAuthId, id);
}
export async function hasPendingInvite(email) {
if (!email || typeof email !== 'string') return false;
const normalized = email.trim().toLowerCase();
const provider = await getProvider();
const result = await provider.query(
'SELECT 1 AS found FROM autoinvite WHERE email = @email',
{ email: normalized }
);
return result.rows.length > 0;
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.create(googleAuthId, data); return store.carrier.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.update(googleAuthId, id, data); return store.carrier.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.order(googleAuthId, data); return store.carrier.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.carrier.delete(googleAuthId, id); return store.carrier.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore, getProvider } from './store.js';
import { requireCompanyAdmin } from './companyuser.js';
export async function listCompanies(authId) { export async function listCompanies(authId) {
const store = await getStore(); const store = await getStore();
@@ -15,12 +16,18 @@ export async function createCompany(googleAuthId, data) {
return store.company.create({ ...data, googleAuthId }); return store.company.create({ ...data, googleAuthId });
} }
export async function updateCompany(id, data) { export async function updateCompany(id, data, googleAuthId) {
if (googleAuthId != null) {
await requireCompanyAdmin(googleAuthId, id);
}
const store = await getStore(); const store = await getStore();
return store.company.update(id, data); return store.company.update(id, data);
} }
export async function deleteCompany(companyId) { export async function deleteCompany(companyId, googleAuthId) {
if (googleAuthId != null) {
await requireCompanyAdmin(googleAuthId, companyId);
}
const store = await getStore(); const store = await getStore();
await store.company.delete(companyId); await store.company.delete(companyId);
} }
@@ -34,3 +41,31 @@ export async function addCompanyHistory(googleAuthId, data) {
const store = await getStore(); const store = await getStore();
return store.company.addHistory(googleAuthId, data); return store.company.addHistory(googleAuthId, data);
} }
export async function isInvitedUser(googleAuthId) {
const provider = await getProvider();
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite
? `SELECT 1 AS is_creator
FROM companyuser cu
WHERE cu.google_auth_id = @googleAuthId
AND cu.id = (SELECT MIN(cu2.id) FROM companyuser cu2 WHERE cu2.company_id = cu.company_id)
LIMIT 1`
: `SELECT TOP 1 1 AS is_creator
FROM companyuser cu
WHERE cu.google_auth_id = @googleAuthId
AND cu.id = (SELECT MIN(cu2.id) FROM companyuser cu2 WHERE cu2.company_id = cu.company_id)`;
const creatorResult = await provider.query(sql, { googleAuthId });
if (creatorResult.rows.length > 0) {
return false;
}
const memberResult = await provider.query(
isSqlite
? `SELECT 1 AS is_member FROM companyuser WHERE google_auth_id = @googleAuthId LIMIT 1`
: `SELECT TOP 1 1 AS is_member FROM companyuser WHERE google_auth_id = @googleAuthId`,
{ googleAuthId }
);
return memberResult.rows.length > 0;
}

View File

@@ -1,4 +1,15 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export const ADMIN_ONLY_SETTING_KEYS = new Set([
'ui.theme',
'keys.tavily',
'keys.openrouter',
'keys.serper',
'modelslugimage',
'systemprompt-prefix',
'systemprompt-suffix',
]);
export async function list(googleAuthId, options) { export async function list(googleAuthId, options) {
const store = await getStore(); const store = await getStore();
@@ -11,6 +22,9 @@ export async function getByKey(googleAuthId, key, options) {
} }
export async function set(googleAuthId, input) { export async function set(googleAuthId, input) {
if (ADMIN_ONLY_SETTING_KEYS.has(input?.key)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
const store = await getStore(); const store = await getStore();
return store.companysetting.set(googleAuthId, input); return store.companysetting.set(googleAuthId, input);
} }

70
src/db/companyuser.js Normal file
View File

@@ -0,0 +1,70 @@
import { getStore, getProvider } from './store.js';
export async function resolveCompanyAccess(googleAuthId, companyId) {
const provider = await getProvider();
const access = await provider.query(
`SELECT company_id, role FROM companyuser WHERE company_id = @companyId AND google_auth_id = @googleAuthId`,
{ companyId, googleAuthId }
);
const accessRow = access.rows[0] ?? null;
if (!accessRow) {
const exists = await provider.query(
`SELECT id FROM company WHERE id = @companyId`,
{ companyId }
);
if (!exists.rows[0]) {
throw new Error('Company not found');
}
throw new Error('Not authorized');
}
return {
companyId: accessRow.company_id,
role: accessRow.role ?? 'user',
};
}
export async function resolveSelectedCompanyAccess(googleAuthId) {
const store = await getStore();
const user = await store.google_auth.getById(googleAuthId);
const companyId = user?.selectedCompanyId ?? null;
if (companyId == null) {
throw new Error('No company selected');
}
return resolveCompanyAccess(googleAuthId, companyId);
}
export async function requireCompanyAdmin(googleAuthId, companyId) {
const ctx = await resolveCompanyAccess(googleAuthId, companyId);
if (ctx.role !== 'admin') {
throw new Error('Not authorized');
}
return ctx;
}
export async function requireSelectedCompanyAdmin(googleAuthId) {
const ctx = await resolveSelectedCompanyAccess(googleAuthId);
if (ctx.role !== 'admin') {
throw new Error('Not authorized');
}
return ctx;
}
export async function list(googleAuthId) {
const store = await getStore();
return store.companyuser.list(googleAuthId);
}
export async function addByEmail(googleAuthId, data) {
const store = await getStore();
return store.companyuser.addByEmail(googleAuthId, data);
}
export async function setRole(googleAuthId, id, role) {
const store = await getStore();
return store.companyuser.setRole(googleAuthId, id, role);
}
export async function remove(googleAuthId, id) {
const store = await getStore();
return store.companyuser.remove(googleAuthId, id);
}

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,21 +7,25 @@ export async function list(googleAuthId) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.create(googleAuthId, data); return store.customergroup.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.update(googleAuthId, id, data); return store.customergroup.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.order(googleAuthId, data); return store.customergroup.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.customergroup.delete(googleAuthId, id); return store.customergroup.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,6 +7,7 @@ export async function list(googleAuthId) {
} }
export async function update(googleAuthId, id, input) { export async function update(googleAuthId, id, input) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.numbers.update(googleAuthId, id, input); return store.numbers.update(googleAuthId, id, input);
} }

View File

@@ -1,4 +1,5 @@
import { getStore, getProvider } from './store.js'; import { getStore, getProvider } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,11 +7,13 @@ export async function list(googleAuthId) {
} }
export async function upsert(googleAuthId, data) { export async function upsert(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.posterminal.upsert(googleAuthId, data); return store.posterminal.upsert(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.posterminal.delete(googleAuthId, id); return store.posterminal.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.create(googleAuthId, data); return store.printer.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.update(googleAuthId, id, data); return store.printer.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.order(googleAuthId, data); return store.printer.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printer.delete(googleAuthId, id); return store.printer.delete(googleAuthId, id);
} }

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -6,16 +7,19 @@ export async function list(googleAuthId) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printeragent.create(googleAuthId, data); return store.printeragent.create(googleAuthId, data);
} }
export async function update(googleAuthId, data) { export async function update(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printeragent.update(googleAuthId, data); return store.printeragent.update(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.printeragent.delete(googleAuthId, id); return store.printeragent.delete(googleAuthId, id);
} }

27
src/db/printerjob.js Normal file
View File

@@ -0,0 +1,27 @@
import { getStore } from './store.js';
export async function create(googleAuthId, input) {
const store = await getStore();
return store.printerjob.create(googleAuthId, input);
}
export async function getById(auth, id) {
const store = await getStore();
return store.printerjob.getById(auth, id);
}
export async function list(auth, filters) {
const store = await getStore();
return store.printerjob.list(auth, filters);
}
export async function updateState(auth, id, state) {
const store = await getStore();
return store.printerjob.updateState(auth, id, state);
}
export async function deleteJob(googleAuthId, id) {
const store = await getStore();
return store.printerjob.delete(googleAuthId, id);
}

View File

@@ -14,6 +14,7 @@ let schemaVersion;
const STORE_NAMESPACES = [ const STORE_NAMESPACES = [
'companyuser', 'companyuser',
'autoinvite',
'company', 'company',
'category', 'category',
'article', 'article',
@@ -42,6 +43,9 @@ const STORE_NAMESPACES = [
'ledger', 'ledger',
'warehouse', 'warehouse',
'printer', 'printer',
'printeragent',
'agentprinter',
'printerjob',
'labeltemplate', 'labeltemplate',
'stock', 'stock',
'propertyname', 'propertyname',

View File

@@ -1,4 +1,5 @@
import { getStore } from './store.js'; import { getStore } from './store.js';
import { requireSelectedCompanyAdmin } from './companyuser.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -11,21 +12,25 @@ export async function getById(googleAuthId, id) {
} }
export async function create(googleAuthId, data) { export async function create(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.create(googleAuthId, data); return store.warehouse.create(googleAuthId, data);
} }
export async function update(googleAuthId, id, data) { export async function update(googleAuthId, id, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.update(googleAuthId, id, data); return store.warehouse.update(googleAuthId, id, data);
} }
export async function order(googleAuthId, data) { export async function order(googleAuthId, data) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.order(googleAuthId, data); return store.warehouse.order(googleAuthId, data);
} }
export async function remove(googleAuthId, id) { export async function remove(googleAuthId, id) {
await requireSelectedCompanyAdmin(googleAuthId);
const store = await getStore(); const store = await getStore();
return store.warehouse.delete(googleAuthId, id); return store.warehouse.delete(googleAuthId, id);
} }

View File

@@ -56,21 +56,13 @@ function getProjectRoot() {
? JSON.parse(fs.readFileSync(nativeMapFile, 'utf8')) ? JSON.parse(fs.readFileSync(nativeMapFile, 'utf8'))
: {}; : {};
// Redirect native .node loading to distDir and preload libvips from distDir if present // Redirect native .node loading to distDir if present
const origDlopen = process.dlopen; const origDlopen = process.dlopen;
process.dlopen = function (mod, filename, flags) { process.dlopen = function (mod, filename, flags) {
const relFromRuntime = path.relative(runtimeDir, filename); const relFromRuntime = path.relative(runtimeDir, filename);
const mappedName = nativeNodes[relFromRuntime] || path.basename(filename); const mappedName = nativeNodes[relFromRuntime] || path.basename(filename);
const distCandidate = path.join(distDir, mappedName); const distCandidate = path.join(distDir, mappedName);
const target = fs.existsSync(distCandidate) ? distCandidate : filename; const target = fs.existsSync(distCandidate) ? distCandidate : filename;
if (mappedName.startsWith('sharp-')) {
const vipsPath = path.join(distDir, 'libvips-cpp.so.8.18.7');
if (fs.existsSync(vipsPath)) {
try {
origDlopen.call(process, { exports: {} }, vipsPath, os.constants.dlopen.RTLD_NOW | os.constants.dlopen.RTLD_GLOBAL);
} catch {}
}
}
return flags !== undefined ? origDlopen.call(process, mod, target, flags) : origDlopen.call(process, mod, target); return flags !== undefined ? origDlopen.call(process, mod, target, flags) : origDlopen.call(process, mod, target);
}; };

View File

@@ -116,7 +116,6 @@ async function snapshotSqliteDatabase(targetFile) {
fs.unlinkSync(targetFile); fs.unlinkSync(targetFile);
} }
try {
const provider = await getProvider(); const provider = await getProvider();
if (provider?.db && typeof provider.db.backup === 'function') { if (provider?.db && typeof provider.db.backup === 'function') {
await provider.db.backup(targetFile); await provider.db.backup(targetFile);
@@ -130,11 +129,8 @@ async function snapshotSqliteDatabase(targetFile) {
const stats = fs.statSync(targetFile); const stats = fs.statSync(targetFile);
return { success: true, sizeBytes: stats.size }; return { success: true, sizeBytes: stats.size };
} }
} catch (err) {
backupLog.warn(`Online SQLite backup method failed (${err.message}), falling back to direct copy`);
}
// Direct file fallback // Direct file copy when provider is offline / not connected
const { sqlitePath } = resolveBackupPaths(); const { sqlitePath } = resolveBackupPaths();
if (fs.existsSync(sqlitePath)) { if (fs.existsSync(sqlitePath)) {
fs.copyFileSync(sqlitePath, targetFile); fs.copyFileSync(sqlitePath, targetFile);

View File

@@ -0,0 +1,272 @@
import { emit } from '../api/sse.js';
import { createLogger } from '../util/logger.js';
const log = createLogger('printer');
/**
* Global in-memory registry of live printer states per company.
* Structure: Map<companyId, Map<agentId, Array<LivePrinter>>>
*
* @typedef {object} LivePrinter
* @property {number} agentId
* @property {string|null} agentName
* @property {string} updatedAt
* @property {string} [model]
* @property {string} [serial]
* @property {string} [name]
* @property {string} [ip]
* @property {string} [address]
* @property {number} [labelLength]
* @property {object} [errors]
* @property {boolean} [ready]
* @property {string} [status]
*/
/** @type {Map<number, Map<number, LivePrinter[]>>} */
const companyAgents = new Map();
/**
* Normalizes an array of printer objects reported by an agent and registers them for the given company.
* Broadcasts the updated whole company printer state via SSE.
*
* @param {number} companyId
* @param {number} agentId
* @param {string|null} agentName
* @param {Array<object>} rawPrinters
* @returns {LivePrinter[]} The complete list of live printers for this company across all agents.
*/
export function updateAgentPrinters(companyId, agentId, agentName, rawPrinters) {
const cId = Number(companyId);
const aId = Number(agentId);
if (!Number.isInteger(cId) || cId <= 0 || !Number.isInteger(aId) || aId <= 0) {
return [];
}
if (!companyAgents.has(cId)) {
companyAgents.set(cId, new Map());
}
const now = new Date().toISOString();
const printerList = Array.isArray(rawPrinters) ? rawPrinters : [];
const normalized = printerList.map((p) => {
const liveStatus = extractLiveStatus(p);
return {
...p,
agentId: aId,
agentName: agentName || null,
updatedAt: now,
...liveStatus,
};
});
companyAgents.get(cId).set(aId, normalized);
const allCompanyPrinters = getCompanyPrinters(cId);
log.info(`Updated ${normalized.length} live printer(s) for company ${cId} (agent ${aId}, total ${allCompanyPrinters.length})`);
broadcastCompanyPrinters(cId, allCompanyPrinters);
return allCompanyPrinters;
}
/**
* Removes all printers associated with a disconnected agent and broadcasts the updated company printer state.
*
* @param {number} companyId
* @param {number} agentId
* @returns {LivePrinter[]} The remaining live printers for this company.
*/
export function removeAgentPrinters(companyId, agentId) {
const cId = Number(companyId);
const aId = Number(agentId);
if (!Number.isInteger(cId) || cId <= 0 || !Number.isInteger(aId) || aId <= 0) {
return [];
}
const agentsMap = companyAgents.get(cId);
if (!agentsMap || !agentsMap.has(aId)) {
return getCompanyPrinters(cId);
}
const removedCount = agentsMap.get(aId)?.length || 0;
agentsMap.delete(aId);
if (agentsMap.size === 0) {
companyAgents.delete(cId);
}
const remainingPrinters = getCompanyPrinters(cId);
log.info(`Removed ${removedCount} live printer(s) for disconnected agent ${aId} from company ${cId} (${remainingPrinters.length} remaining)`);
broadcastCompanyPrinters(cId, remainingPrinters);
return remainingPrinters;
}
/**
* Returns all active discovered printers for a company across all reporting agents.
*
* @param {number} companyId
* @returns {LivePrinter[]}
*/
export function getCompanyPrinters(companyId) {
const cId = Number(companyId);
const agentsMap = companyAgents.get(cId);
if (!agentsMap) return [];
const result = [];
for (const printers of agentsMap.values()) {
result.push(...printers);
}
return result;
}
/**
* Finds a single live printer by ID, serial, IP/address, or name.
*
* @param {number} companyId
* @param {string|number} identifier
* @returns {LivePrinter|null}
*/
export function getCompanyPrinter(companyId, identifier) {
if (identifier == null) return null;
const key = String(identifier).trim().toLowerCase();
if (!key) return null;
const printers = getCompanyPrinters(companyId);
return printers.find((p) => {
if (p.id != null && String(p.id).toLowerCase() === key) return true;
if (p.serial && String(p.serial).toLowerCase() === key) return true;
if (p.ip && String(p.ip).toLowerCase() === key) return true;
if (p.address && String(p.address).toLowerCase() === key) return true;
if (p.name && String(p.name).toLowerCase() === key) return true;
return false;
}) || null;
}
/**
* Broadcasts the current live printer list for a company to all connected subscribers via SSE.
* Emits `agentprinter` with data `{ printers }`.
*
* @param {number} companyId
* @param {LivePrinter[]} [printers]
*/
export function broadcastCompanyPrinters(companyId, printers) {
const cId = Number(companyId);
if (!Number.isInteger(cId) || cId <= 0) return;
const list = printers ?? getCompanyPrinters(cId);
const payload = { printers: list };
emit('agentprinter', { companyId: cId, data: payload });
}
/**
* Extracts normalized boolean live status indicators and telemetry from live printer telemetry.
*
* @param {object|null|undefined} live
* @returns {{ open: boolean, pause: boolean, error: boolean, online: boolean, labelLength: number|null }}
*/
export function extractLiveStatus(live) {
if (!live) {
return {
open: false,
pause: false,
error: false,
online: false,
labelLength: null,
};
}
const errors = live.errors || {};
const isOffline = live.status === 'offline' || live.unreachable === true || errors.unreachable === true;
const online = !isOffline;
const open = Boolean(live.open || errors.headOpen);
const pause = Boolean(live.pause || errors.pause);
const hasOtherError = Boolean(
live.error ||
errors.other ||
Object.entries(errors).some(([key, val]) => !['headOpen', 'pause', 'unreachable'].includes(key) && Boolean(val))
);
const labelLength = live.labelLength != null && Number.isFinite(Number(live.labelLength))
? Number(live.labelLength)
: null;
return {
open,
pause,
error: hasOtherError,
online,
labelLength,
};
}
/**
* Finds a matching live printer from memory for a given database agentprinter record.
*
* @param {number} companyId
* @param {object} agentprinter
* @returns {LivePrinter|null}
*/
export function getLivePrinterForAgentPrinter(companyId, agentprinter) {
const printers = getCompanyPrinters(companyId);
if (!agentprinter || printers.length === 0) return null;
if (agentprinter.id != null) {
const match = printers.find((p) => p.agentprinterId === agentprinter.id);
if (match) return match;
}
if (agentprinter.printeragentId && agentprinter.serial) {
const match = printers.find((p) =>
Number(p.agentId) === Number(agentprinter.printeragentId) &&
p.serial &&
String(p.serial).trim().toLowerCase() === String(agentprinter.serial).trim().toLowerCase()
);
if (match) return match;
}
if (agentprinter.printeragentId && agentprinter.model) {
const match = printers.find((p) =>
Number(p.agentId) === Number(agentprinter.printeragentId) &&
p.model &&
String(p.model).trim().toLowerCase() === String(agentprinter.model).trim().toLowerCase() &&
(!agentprinter.type || !p.type || String(p.type).trim().toLowerCase() === String(agentprinter.type).trim().toLowerCase())
);
if (match) return match;
}
return null;
}
/**
* Enriches an agentprinter database record with live operational status (open, pause, error, online).
*
* @param {number} companyId
* @param {object} agentprinter
* @returns {object}
*/
export function enrichAgentPrinter(companyId, agentprinter) {
if (!agentprinter) return agentprinter;
const live = getLivePrinterForAgentPrinter(companyId, agentprinter);
const status = extractLiveStatus(live);
return {
...agentprinter,
...status,
};
}
/**
* Clears all live printers for a company (e.g. on company deletion).
*
* @param {number} companyId
*/
export function clearCompanyPrinters(companyId) {
const cId = Number(companyId);
if (companyAgents.has(cId)) {
companyAgents.delete(cId);
broadcastCompanyPrinters(cId, []);
}
}

View File

@@ -0,0 +1,72 @@
import * as labeltemplateDb from '../db/labeltemplate.js';
import { renderThumb } from './labelThumb.js';
const TEXTBOX_DEFAULTS = {
blackpoint: 128,
rotation: 0,
marginBottom: 0,
};
const DEFAULT_TEMPLATES = [
{
name: 'FD057X032Z1',
width: 448,
height: 256,
dpi: 203,
type: 'zpl',
content: {
objects: [
{
id: '4df08336-e936-40bf-b9d9-c7b08c5a1ad6', type: 'textbox', text: '{{name}}', font: 'outfit',
...TEXTBOX_DEFAULTS, minFontSize: 14, maxFontSize: 120, halign: 'center', valign: 'top',
invert: false, cornerRadius: 0, marginLeft: 0, marginTop: 0, marginRight: 0,
x: 9, y: 9, w: 427, h: 133,
},
{
id: 'a2037b05-8124-4a4d-85d1-270a7f9cdb8a', type: 'textbox', text: '{{baseprice}}', font: 'barlow-semi-condensed',
...TEXTBOX_DEFAULTS, minFontSize: 1, maxFontSize: 18, halign: 'right', valign: 'top',
invert: false, cornerRadius: 0, marginLeft: 0, marginTop: 0, marginRight: 0,
x: 12, y: 138, w: 188, h: 20,
},
{
id: '50e81524-04d3-4fa2-bca6-e5f346c27907', type: 'barcode',
x: 10, y: 162, h: 52, scale: 2, format: 'EAN13', code: '0',
},
{
id: '16443d4f-8d86-4253-bfbf-e98c44e6552a', type: 'textbox', text: '{{price}}', font: 'barlow-semi-condensed',
...TEXTBOX_DEFAULTS, minFontSize: 14, maxFontSize: 88, halign: 'center', valign: 'middle',
invert: true, cornerRadius: 30, marginLeft: 10, marginTop: 5, marginRight: 10,
x: 214, y: 146, w: 220, h: 83,
},
],
},
},
{
name: 'E025X013Z1',
width: 200,
height: 104,
dpi: 203,
type: 'zpl',
content: {
objects: [
{
id: 'd666154d-6571-41fb-ad50-1650a6218deb', type: 'textbox', text: '{{price}}', font: 'barlow-semi-condensed',
...TEXTBOX_DEFAULTS, minFontSize: 14, maxFontSize: 66, halign: 'center', valign: 'middle',
invert: true, cornerRadius: 24, marginLeft: 12, marginTop: 6, marginRight: 12,
x: 109, y: 2, w: 210, h: 91,
},
],
},
},
];
/**
* Creates the default label templates for the user's (newly selected) company.
* @param {number} googleAuthId
*/
export async function createDefaultLabelTemplates(googleAuthId) {
for (const tpl of DEFAULT_TEMPLATES) {
const thumb = await renderThumb({ width: tpl.width, height: tpl.height, objects: tpl.content.objects });
await labeltemplateDb.create(googleAuthId, { ...tpl, thumb });
}
}

View File

@@ -344,8 +344,8 @@ export async function runSandboxJob(options) {
throw new Error('Sandbox worker pool is shutting down'); throw new Error('Sandbox worker pool is shutting down');
} }
const { poolKey, googleAuthId, code, timeoutMs, dashbackToken, appletInput, appletFile } = options; const { poolKey, googleAuthId, isAdmin = true, code, timeoutMs, dashbackToken, appletInput, appletFile } = options;
const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId }; const job = { code, timeoutMs, dashbackToken, appletInput, appletFile, poolKey, googleAuthId, isAdmin };
const entry = await acquireWorker(poolKey, googleAuthId); const entry = await acquireWorker(poolKey, googleAuthId);
if (entry) { if (entry) {

View File

@@ -0,0 +1,76 @@
import { optimizeImage } from 'wasm-image-optimization';
/**
* Converts a PNG image buffer into Zebra ZPL II Graphic Field (^GFA) format.
* Uses Zebra's alternative compression scheme (ACS run-length encoding on hex)
* to minimize payload size over the network and serial/USB ports.
*
* @param {Buffer} pngBuffer - Rendered 1-bit or RGBA PNG buffer.
* @param {number} [quantity=1] - Number of copies to print (^PQ).
* @returns {Promise<string>} Full ZPL string ready to send to a Zebra printer.
*/
export async function pngToZpl(pngBuffer, quantity = 1) {
const raw = await optimizeImage({ image: pngBuffer, format: 'raw' });
const { width: w, height: h, data } = raw;
const bytesPerRow = Math.ceil(w / 8);
const totalBytes = bytesPerRow * h;
const buffer = new Uint8Array(totalBytes);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const idx = (y * w + x) * 4;
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
const a = data[idx + 3];
// Ink threshold: non-transparent and dark pixels are black (1)
const isBlack = a > 128 && (r * 0.299 + g * 0.587 + b * 0.114) < 128;
if (isBlack) {
const byteIdx = y * bytesPerRow + Math.floor(x / 8);
const bitIdx = 7 - (x % 8);
buffer[byteIdx] |= 1 << bitIdx;
}
}
}
const hexMap = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0').toUpperCase());
let hex = '';
for (let i = 0; i < totalBytes; i++) {
hex += hexMap[buffer[i]];
}
// Zebra ACS run-length encoding:
// Repeated characters are replaced by letters:
// 'G'..'Y' = 1..19 repeats
// 'g'..'y' = 20..380 repeats (step 20)
// 'z' = 400 repeats
let acs = '';
const re = /([0-9A-F])\1{2,}/g;
let match = re.exec(hex);
let offset = 0;
while (match) {
acs += hex.substring(offset, match.index);
let l = match[0].length;
while (l >= 400) {
acs += 'z';
l -= 400;
}
if (l >= 20) {
acs += '_ghijklmnopqrstuvwxy'[Math.floor(l / 20)];
l = l % 20;
}
if (l) {
acs += '_GHIJKLMNOPQRSTUVWXY'[l];
}
acs += match[1];
offset = re.lastIndex;
match = re.exec(hex);
}
acs += hex.substring(offset);
const qty = Number(quantity);
const pq = Number.isInteger(qty) && qty > 1 ? `^PQ${qty}\n` : '';
return `^XA^PW${w}^LL${h}^LS0^FT0,0^GFA,${totalBytes},${totalBytes},${bytesPerRow},${acs}\n${pq}^XZ`;
}

View File

@@ -4,7 +4,7 @@ import { optimizeImage } from 'wasm-image-optimization';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { getImageByHash } from '../db/pic.js'; import { getImageByHash } from '../db/pic.js';
import { prepareVfs, syncVfsToDb, extractFullHash, isReadOnlyDatabasePath } from './vfs.js'; import { prepareVfs, extractFullHash, isReadOnlyDatabasePath } from './vfs.js';
import { resolveOpenRouterKey, resolveImageModel } from './llm-key.js'; import { resolveOpenRouterKey, resolveImageModel } from './llm-key.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
@@ -482,11 +482,7 @@ export function createEditImageTool({ googleAuthId, model: fallbackModel }) {
fs.mkdirSync(path.dirname(absTarget), { recursive: true }); fs.mkdirSync(path.dirname(absTarget), { recursive: true });
fs.writeFileSync(absTarget, finalBuffer); fs.writeFileSync(absTarget, finalBuffer);
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (syncErr) {
console.warn('[edit_image] warning: failed to sync output image to modelvfs db:', syncErr.message);
}
const outFinalRel = path.relative(vfsRoot, absTarget).replace(/\\/g, '/'); const outFinalRel = path.relative(vfsRoot, absTarget).replace(/\\/g, '/');

View File

@@ -544,7 +544,7 @@ async function runJob(job) {
const appletInput = job.appletInput; const appletInput = job.appletInput;
const appletFile = job.appletFile; const appletFile = job.appletFile;
const vfs = createVfs(vfsDir, { googleAuthId }); const vfs = createVfs(vfsDir, { googleAuthId, isAdmin: job.isAdmin !== false });
const bridgedExcel = bridgeExcelJS(vfs); const bridgedExcel = bridgeExcelJS(vfs);
const bridgedPDF = bridgePDFKit(vfs); const bridgedPDF = bridgePDFKit(vfs);

View File

@@ -4,7 +4,8 @@ import jwt from 'jsonwebtoken';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { createVfs, prepareVfs, syncVfsToDb } from './vfs.js'; import { createVfs, prepareVfs } from './vfs.js';
import { resolveSelectedCompanyAccess } from '../db/companyuser.js';
import { runSandboxJob } from '../services/javascriptSandbox.js'; import { runSandboxJob } from '../services/javascriptSandbox.js';
const DEFAULT_TIMEOUT_MS = 20_000; const DEFAULT_TIMEOUT_MS = 20_000;
@@ -184,11 +185,21 @@ function toTypedCsv(result, columns) {
* *
* @param {{ code: string, timeoutMs: number, vfsRoot: string, googleAuthId: number, appletInput?: object, appletFile?: string }} ctx * @param {{ code: string, timeoutMs: number, vfsRoot: string, googleAuthId: number, appletInput?: object, appletFile?: string }} ctx
*/ */
function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) { async function runInWorker({ code, timeoutMs, vfsRoot, googleAuthId, appletInput, appletFile }) {
let isAdmin = true;
if (googleAuthId != null) {
try {
const access = await resolveSelectedCompanyAccess(googleAuthId);
isAdmin = access.role === 'admin';
} catch {
isAdmin = false;
}
}
const dashbackToken = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' }); const dashbackToken = jwt.sign({ id: googleAuthId }, env.jwtSecret, { expiresIn: '5m' });
return runSandboxJob({ return runSandboxJob({
poolKey: vfsRoot, poolKey: vfsRoot,
googleAuthId, googleAuthId,
isAdmin,
code, code,
timeoutMs, timeoutMs,
dashbackToken, dashbackToken,
@@ -367,9 +378,7 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
const isApplet = hasApplet; const isApplet = hasApplet;
const applet = isApplet ? readApplet(vfsRoot, appletvfsfile) : null; const applet = isApplet ? readApplet(vfsRoot, appletvfsfile) : null;
const source = isApplet ? applet.source : code; const source = isApplet ? applet.source : code;
let report; const report = await runInWorker({
try {
report = await runInWorker({
code: source, code: source,
timeoutMs: hardTimeout, timeoutMs: hardTimeout,
vfsRoot, vfsRoot,
@@ -377,9 +386,6 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
appletInput: isApplet ? (applet_input ?? {}) : undefined, appletInput: isApplet ? (applet_input ?? {}) : undefined,
appletFile: isApplet ? appletvfsfile.trim() : undefined, appletFile: isApplet ? appletvfsfile.trim() : undefined,
}); });
} finally {
await syncVfsToDb({ googleAuthId, vfsRoot });
}
if (report.timedOut) { if (report.timedOut) {
throw new ToolError( throw new ToolError(

View File

@@ -1,9 +1,9 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath } from './vfs.js'; import { isReadOnlyDatabasePath, isProtectedAdminVfsPath, prepareVfs } from './vfs.js';
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js'; import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
import { prepareVfs, syncVfsToDb } from './vfs.js';
import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { getStore } from '../db/store.js'; import { getStore } from '../db/store.js';
@@ -155,6 +155,10 @@ export function createPatchTool({ googleAuthId }) {
); );
} }
if (isProtectedAdminVfsPath(clean)) {
await requireSelectedCompanyAdmin(googleAuthId);
}
// A SKILL file must be flat — reject an attempted nested path up front. // A SKILL file must be flat — reject an attempted nested path up front.
const looksSkillByExt = clean.toLowerCase().endsWith(SKILL_EXT.toLowerCase()); const looksSkillByExt = clean.toLowerCase().endsWith(SKILL_EXT.toLowerCase());
if (looksSkillByExt && clean.includes('/')) { if (looksSkillByExt && clean.includes('/')) {
@@ -262,17 +266,7 @@ export function createPatchTool({ googleAuthId }) {
if (isSkill) touchedSkills.add(clean); if (isSkill) touchedSkills.add(clean);
} }
if (applied.length > 0) {
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (err) {
console.error('[patch] failed to sync VFS to db:', err.message);
throw new ToolError(
`Patch applied to disk but could not be persisted: ${err.message}. ` +
`Files changed: ${applied.join(', ')}.`
);
}
}
const result = { const result = {
success: true, success: true,

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm'; import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm';
import { prepareVfs, syncVfsToDb } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
const IMAGE_FOLDER = 'images'; const IMAGE_FOLDER = 'images';
@@ -186,13 +186,7 @@ export function createPdfReadTool({ googleAuthId }) {
}); });
} }
if (savedImages.length > 0) {
try {
await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (err) {
console.warn('[pdf_read] failed to sync rendered images to VFS DB:', err?.message || err);
}
}
result.images = savedImages; result.images = savedImages;
if (!result.totalPages) result.totalPages = images.length; if (!result.totalPages) result.totalPages = images.length;

Some files were not shown because too many files have changed in this diff Show More