This commit is contained in:
seb
2026-10-31 21:02:53 +01:00
parent 5a0b603306
commit dd5c9646b3
23 changed files with 1462 additions and 223 deletions

View File

@@ -2,6 +2,7 @@ import express from 'express';
import crypto from 'node:crypto';
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 { emit } from '../sse.js';
import { createLogger } from '../../util/logger.js';
@@ -202,6 +203,36 @@ async function handlePair(req, res) {
agentRouter.post('/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);
if (!conns || conns.size === 0) 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.write(`event: printerjob\ndata: ${payload}\n\n`);
}
}
return true;
}
/**
* @openapi
* /agent/events:
@@ -256,6 +287,12 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
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`);
const keepAlive = setInterval(() => {
@@ -269,6 +306,13 @@ agentRouter.get('/events', agentAuthMiddleware, async (req, res) => {
if (closed) return;
closed = true;
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) {
@@ -393,3 +437,78 @@ agentRouter.get('/state', agentAuthMiddleware, (req, res) => {
});
});
// #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');
if (req.agent?.companyId) {
emit('printerjob', { companyId: req.agent.companyId, data: { job } });
}
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');
if (req.agent?.companyId) {
emit('printerjob', { companyId: req.agent.companyId, data: { job } });
}
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);
if (req.agent?.companyId) {
emit('printerjob', { companyId: req.agent.companyId, data: { job } });
}
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

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

View File

@@ -1774,6 +1774,59 @@
* 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:
* type: object
* required: [id, type, x, y]

View File

@@ -1,5 +1,6 @@
import * as db from '../../db/agentprinter.js';
import { emit } from '../sse.js';
import { enrichAgentPrinter } from '../../services/companyPrinterService.js';
// #region Helpers
@@ -91,7 +92,8 @@ export function register(app, basePath = '') {
try {
const printeragentId = req.query?.printeragentId ? Number(req.query.printeragentId) : undefined;
const agentprinters = await db.list(req.user.id, printeragentId);
res.status(200).json({ agentprinters });
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);
@@ -236,7 +238,7 @@ export function register(app, basePath = '') {
if (!agentprinter) {
return res.status(404).json({ error: 'Agentprinter not found' });
}
res.status(200).json({ agentprinter });
res.status(200).json({ agentprinter: enrichAgentPrinter(selected.companyId, agentprinter) });
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('Get agentprinter failed:', error);
@@ -333,7 +335,7 @@ export function register(app, basePath = '') {
try {
const agentprinter = await db.update(req.user.id, payload);
emit('agentprinter', { companyId: selected.companyId });
res.status(200).json({ agentprinter });
res.status(200).json({ agentprinter: enrichAgentPrinter(selected.companyId, agentprinter) });
} catch (error) {
const status = agentprinterErrorStatus(error);
if (status === 500) console.error('Update agentprinter failed:', error);

View File

@@ -57,6 +57,7 @@ export function register(app, basePath = '') {
* | `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 |
* | `printerjob` | `{ job }` or `{ jobId, state }` | `POST /zpljob`, `POST /printerjob`, `POST /printerjob/{id}/cancel`, `DELETE /printerjob/{id}`, agent job state updates |
* | `warehouse` | `{}` | `POST /warehouse`, `PUT /warehouse/{id}`, `PATCH /warehouse/{id}`, `DELETE /warehouse/{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}` |
@@ -99,6 +100,12 @@ export function register(app, basePath = '') {
* payload: "{}"
* - name: printer
* payload: "{}"
* - name: printeragent
* payload: "{}"
* - name: agentprinter
* payload: "{ printers } or {}"
* - name: printerjob
* payload: "{ job } or { jobId, state }"
* - name: warehouse
* payload: "{}"
* - name: supplier

View File

@@ -0,0 +1,388 @@
import * as printerjobDb from '../../db/printerjob.js';
import { emit } from '../sse.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: []
* x-signals: [printerjob]
* 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 ?? 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);
}
emit('printerjob', { companyId: selected.companyId, data: { 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/{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: []
* x-signals: [printerjob]
* 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);
}
emit('printerjob', { companyId: selected.companyId, data: { 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: []
* x-signals: [printerjob]
* 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);
}
emit('printerjob', {
companyId: selected.companyId,
data: { jobId: job.id, state: 'cancelled' },
});
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
}

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

@@ -0,0 +1,176 @@
import * as printerjobDb from '../../db/printerjob.js';
import { emit } from '../sse.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: []
* x-signals: [printerjob]
* 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 rawApId = req.body?.agentprinterId ?? 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 rawCopies = req.body?.copiescount ?? req.body?.copiesCount ?? req.body?.copies;
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);
}
emit('printerjob', { companyId: selected.companyId, data: { 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
}

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

@@ -42,6 +42,9 @@ const STORE_NAMESPACES = [
'ledger',
'warehouse',
'printer',
'printeragent',
'agentprinter',
'printerjob',
'labeltemplate',
'stock',
'propertyname',

View File

@@ -48,12 +48,16 @@ export function updateAgentPrinters(companyId, agentId, agentName, rawPrinters)
const now = new Date().toISOString();
const printerList = Array.isArray(rawPrinters) ? rawPrinters : [];
const normalized = printerList.map((p) => ({
...p,
agentId: aId,
agentName: agentName || null,
updatedAt: now,
}));
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);
@@ -155,6 +159,106 @@ export function broadcastCompanyPrinters(companyId, printers) {
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 ?? live.headOpen ?? errors.headOpen ?? false);
const pause = Boolean(live.pause ?? live.paused ?? errors.pause ?? false);
const hasOtherError = Boolean(
live.error ||
errors.other ||
errors.error ||
Object.entries(errors).some(([key, val]) => !['headOpen', 'pause', 'unreachable'].includes(key) && Boolean(val))
);
const rawLabelLength = live.labelLength ?? live.labellength;
const labelLength = rawLabelLength != null && Number.isFinite(Number(rawLabelLength))
? Number(rawLabelLength)
: 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).
*