Added document printer support.
This commit is contained in:
parent
509b7de11c
commit
79ed7691f9
47
src/database/schemas/management/documentjob.schema.js
Normal file
47
src/database/schemas/management/documentjob.schema.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const documentJobSchema = new Schema(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
objectType: { type: String, required: false },
|
||||||
|
object: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
refPath: 'objectType',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: { type: String, required: true, default: 'queued' },
|
||||||
|
percent: { type: Number, required: false },
|
||||||
|
},
|
||||||
|
documentTemplate: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'documentTemplate',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
documentPrinter: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'documentPrinter',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add virtual id getter
|
||||||
|
documentJobSchema.virtual('id').get(function () {
|
||||||
|
return this._id.toHexString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure JSON serialization to include virtuals
|
||||||
|
documentJobSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const documentJobModel = mongoose.model('documentJob', documentJobSchema);
|
||||||
55
src/database/schemas/management/documentprinter.schema.js
Normal file
55
src/database/schemas/management/documentprinter.schema.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const connectionSchema = new Schema(
|
||||||
|
{
|
||||||
|
interface: { type: String, required: true },
|
||||||
|
protocol: { type: String, required: true },
|
||||||
|
host: { type: String, required: true },
|
||||||
|
port: { type: Number, required: false }
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const documentPrinterSchema = new Schema(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
connection: { type: connectionSchema, required: true },
|
||||||
|
currentDocumentSize: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'documentSize',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
tags: [{ type: String }],
|
||||||
|
online: { type: Boolean, required: true, default: false },
|
||||||
|
active: { type: Boolean, required: true, default: true },
|
||||||
|
state: {
|
||||||
|
type: { type: String, required: true, default: 'offline' },
|
||||||
|
message: { type: String, required: false },
|
||||||
|
progress: { type: Number, required: false }
|
||||||
|
},
|
||||||
|
connectedAt: { type: Date, default: null },
|
||||||
|
host: { type: Schema.Types.ObjectId, ref: 'host', required: true },
|
||||||
|
queue: [
|
||||||
|
{ type: Schema.Types.ObjectId, ref: 'documentJob', required: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add virtual id getter
|
||||||
|
documentPrinterSchema.virtual('id').get(function () {
|
||||||
|
return this._id.toHexString();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure JSON serialization to include virtuals
|
||||||
|
documentPrinterSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const documentPrinterModel = mongoose.model(
|
||||||
|
'documentPrinter',
|
||||||
|
documentPrinterSchema
|
||||||
|
);
|
||||||
@ -6,18 +6,23 @@ const documentSizeSchema = new Schema(
|
|||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
default: 0,
|
default: 0
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
default: 0,
|
default: 0
|
||||||
},
|
},
|
||||||
|
infiniteHeight: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
@ -30,4 +35,7 @@ documentSizeSchema.virtual('id').get(function () {
|
|||||||
// Configure JSON serialization to include virtuals
|
// Configure JSON serialization to include virtuals
|
||||||
documentSizeSchema.set('toJSON', { virtuals: true });
|
documentSizeSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
export const documentSizeModel = mongoose.model('documentSize', documentSizeSchema);
|
export const documentSizeModel = mongoose.model(
|
||||||
|
'documentSize',
|
||||||
|
documentSizeSchema
|
||||||
|
);
|
||||||
|
|||||||
@ -2,12 +2,18 @@ import log4js from 'log4js';
|
|||||||
// Load configuration
|
// Load configuration
|
||||||
import { loadConfig } from '../config.js';
|
import { loadConfig } from '../config.js';
|
||||||
import { CodeAuth, createAuthMiddleware } from '../auth/auth.js';
|
import { CodeAuth, createAuthMiddleware } from '../auth/auth.js';
|
||||||
import { editObject, getObject, listObjects } from '../database/database.js';
|
import {
|
||||||
|
newObject,
|
||||||
|
editObject,
|
||||||
|
getObject,
|
||||||
|
listObjects
|
||||||
|
} from '../database/database.js';
|
||||||
import { hostModel } from '../database/schemas/management/host.schema.js';
|
import { hostModel } from '../database/schemas/management/host.schema.js';
|
||||||
import { UpdateManager } from '../updates/updatemanager.js';
|
import { UpdateManager } from '../updates/updatemanager.js';
|
||||||
import { ActionManager } from '../actions/actionmanager.js';
|
import { ActionManager } from '../actions/actionmanager.js';
|
||||||
import { getModelByName } from '../utils.js';
|
import { getModelByName } from '../utils.js';
|
||||||
import { EventManager } from '../events/eventmanager.js';
|
import { EventManager } from '../events/eventmanager.js';
|
||||||
|
import { TemplateManager } from '../templates/templatemanager.js';
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@ -25,6 +31,7 @@ export class SocketHost {
|
|||||||
this.updateManager = new UpdateManager(this);
|
this.updateManager = new UpdateManager(this);
|
||||||
this.actionManager = new ActionManager(this);
|
this.actionManager = new ActionManager(this);
|
||||||
this.eventManager = new EventManager(this);
|
this.eventManager = new EventManager(this);
|
||||||
|
this.templateManager = new TemplateManager(this);
|
||||||
this.codeAuth = new CodeAuth();
|
this.codeAuth = new CodeAuth();
|
||||||
this.setupSocketEventHandlers();
|
this.setupSocketEventHandlers();
|
||||||
}
|
}
|
||||||
@ -34,12 +41,33 @@ export class SocketHost {
|
|||||||
this.socket.on('authenticate', this.handleAuthenticate.bind(this));
|
this.socket.on('authenticate', this.handleAuthenticate.bind(this));
|
||||||
this.socket.on('updateHost', this.handleUpdateHost.bind(this));
|
this.socket.on('updateHost', this.handleUpdateHost.bind(this));
|
||||||
this.socket.on('getObject', this.handleGetObject.bind(this));
|
this.socket.on('getObject', this.handleGetObject.bind(this));
|
||||||
|
this.socket.on('newObject', this.handleNewObject.bind(this));
|
||||||
this.socket.on('editObject', this.handleEditObject.bind(this));
|
this.socket.on('editObject', this.handleEditObject.bind(this));
|
||||||
this.socket.on('listObjects', this.handleListObjects.bind(this));
|
this.socket.on('listObjects', this.handleListObjects.bind(this));
|
||||||
|
this.socket.on(
|
||||||
|
'subscribeToObjectUpdates',
|
||||||
|
this.handleSubscribeToObjectUpdatesEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'unsubscribeToObjectUpdates',
|
||||||
|
this.handleUnsubscribeToObjectUpdatesEvent.bind(this)
|
||||||
|
);
|
||||||
this.socket.on(
|
this.socket.on(
|
||||||
'subscribeToObjectActions',
|
'subscribeToObjectActions',
|
||||||
this.handleSubscribeToObjectActions.bind(this)
|
this.handleSubscribeToObjectActions.bind(this)
|
||||||
);
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'subscribeToObjectEvent',
|
||||||
|
this.handleSubscribeToObjectEventEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'unsubscribeObjectEvent',
|
||||||
|
this.handleUnsubscribeObjectEventEvent.bind(this)
|
||||||
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'renderTemplatePDF',
|
||||||
|
this.handleRenderTemplatePDFEvent.bind(this)
|
||||||
|
);
|
||||||
this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this));
|
this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this));
|
||||||
this.socket.on('disconnect', this.handleDisconnect.bind(this));
|
this.socket.on('disconnect', this.handleDisconnect.bind(this));
|
||||||
}
|
}
|
||||||
@ -107,6 +135,16 @@ export class SocketHost {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleNewObject(data, callback) {
|
||||||
|
const object = await newObject({
|
||||||
|
model: getModelByName(data.objectType),
|
||||||
|
newData: data.newData,
|
||||||
|
owner: this.host,
|
||||||
|
ownerType: 'host'
|
||||||
|
});
|
||||||
|
callback(object);
|
||||||
|
}
|
||||||
|
|
||||||
async handleEditObject(data, callback) {
|
async handleEditObject(data, callback) {
|
||||||
const object = await editObject({
|
const object = await editObject({
|
||||||
model: getModelByName(data.objectType),
|
model: getModelByName(data.objectType),
|
||||||
@ -151,6 +189,13 @@ export class SocketHost {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleSubscribeToObjectUpdatesEvent(data) {
|
||||||
|
const result = await this.updateManager.subscribeToObjectUpdate(
|
||||||
|
data._id,
|
||||||
|
data.objectType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async handleSubscribeToObjectActions(data) {
|
async handleSubscribeToObjectActions(data) {
|
||||||
await this.actionManager.subscribeToObjectActions(
|
await this.actionManager.subscribeToObjectActions(
|
||||||
data._id,
|
data._id,
|
||||||
@ -158,6 +203,78 @@ export class SocketHost {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleSubscribeToObjectEventEvent(data) {
|
||||||
|
await this.eventManager.subscribeToObjectEvent(
|
||||||
|
data._id,
|
||||||
|
data.objectType,
|
||||||
|
data.eventType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleUnsubscribeObjectEventEvent(data) {
|
||||||
|
await this.eventManager.removeObjectEventsListener(
|
||||||
|
data._id,
|
||||||
|
data.objectType,
|
||||||
|
data.eventType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleUnsubscribeToObjectUpdatesEvent(data) {
|
||||||
|
await this.updateManager.unsubscribeToObjectUpdate(
|
||||||
|
data._id,
|
||||||
|
data.objectType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRenderTemplatePDFEvent(data, callback) {
|
||||||
|
const result = await this.templateManager.renderPDF(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.object,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDevicesState(state, online, connectedAt) {
|
||||||
|
logger.info('Setting devices state to', state, 'and online to', online);
|
||||||
|
|
||||||
|
const documentPrinters = await listObjects({
|
||||||
|
model: getModelByName('documentPrinter'),
|
||||||
|
filter: { host: this.host._id }
|
||||||
|
});
|
||||||
|
const printers = await listObjects({
|
||||||
|
model: getModelByName('printer'),
|
||||||
|
filter: { host: this.host._id }
|
||||||
|
});
|
||||||
|
logger.debug(
|
||||||
|
'Retrieved',
|
||||||
|
documentPrinters.length,
|
||||||
|
'document printers and',
|
||||||
|
printers.length,
|
||||||
|
'printers'
|
||||||
|
);
|
||||||
|
for (const documentPrinter of documentPrinters) {
|
||||||
|
await editObject({
|
||||||
|
model: getModelByName('documentPrinter'),
|
||||||
|
id: documentPrinter._id,
|
||||||
|
updateData: { state: state, online: online, connectedAt: connectedAt },
|
||||||
|
owner: this.host,
|
||||||
|
ownerType: 'host'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const printer of printers) {
|
||||||
|
await editObject({
|
||||||
|
model: getModelByName('printer'),
|
||||||
|
id: printer._id,
|
||||||
|
updateData: { state: state, online: online, connectedAt: connectedAt },
|
||||||
|
owner: this.host,
|
||||||
|
ownerType: 'host'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.info('Devices state set to', state, 'and online to', online);
|
||||||
|
}
|
||||||
|
|
||||||
async handleDisconnect() {
|
async handleDisconnect() {
|
||||||
if (this.authenticated) {
|
if (this.authenticated) {
|
||||||
await editObject({
|
await editObject({
|
||||||
@ -173,6 +290,13 @@ export class SocketHost {
|
|||||||
});
|
});
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
}
|
}
|
||||||
|
await this.actionManager.removeAllListeners();
|
||||||
|
await this.eventManager.removeAllListeners();
|
||||||
|
await this.setDevicesState(
|
||||||
|
{ type: 'offline', message: 'Host disconnected.' },
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
);
|
||||||
logger.info('External host disconnected. Socket ID:', this.id);
|
logger.info('External host disconnected. Socket ID:', this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,11 +64,16 @@ export class SocketUser {
|
|||||||
'previewTemplate',
|
'previewTemplate',
|
||||||
this.handlePreviewTemplateEvent.bind(this)
|
this.handlePreviewTemplateEvent.bind(this)
|
||||||
);
|
);
|
||||||
|
this.socket.on(
|
||||||
|
'renderTemplatePDF',
|
||||||
|
this.handleRenderTemplatePDFEvent.bind(this)
|
||||||
|
);
|
||||||
this.socket.on(
|
this.socket.on(
|
||||||
'generateHostOtp',
|
'generateHostOtp',
|
||||||
this.handleGenerateHostOtpEvent.bind(this)
|
this.handleGenerateHostOtpEvent.bind(this)
|
||||||
);
|
);
|
||||||
this.socket.on('objectAction', this.handleObjectActionEvent.bind(this));
|
this.socket.on('objectAction', this.handleObjectActionEvent.bind(this));
|
||||||
|
this.socket.on('disconnect', this.handleDisconnect.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAuthenticateEvent(data, callback) {
|
async handleAuthenticateEvent(data, callback) {
|
||||||
@ -196,6 +201,15 @@ export class SocketUser {
|
|||||||
callback(result);
|
callback(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleRenderTemplatePDFEvent(data, callback) {
|
||||||
|
const result = await this.templateManager.renderPDF(
|
||||||
|
data._id,
|
||||||
|
data.content,
|
||||||
|
data.object,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
async handleGenerateHostOtpEvent(data, callback) {
|
async handleGenerateHostOtpEvent(data, callback) {
|
||||||
const result = await generateHostOTP(data._id);
|
const result = await generateHostOTP(data._id);
|
||||||
callback(result);
|
callback(result);
|
||||||
@ -210,7 +224,9 @@ export class SocketUser {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDisconnect() {
|
async handleDisconnect() {
|
||||||
|
await this.actionManager.removeAllListeners();
|
||||||
|
await this.eventManager.removeAllListeners();
|
||||||
logger.info('External user disconnected:', this.socket.user?.username);
|
logger.info('External user disconnected:', this.socket.user?.username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,26 +15,21 @@
|
|||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
min-width: calc((<%= width || '50mm' %> * <%= scale || '1' %>) + 100px);
|
|
||||||
min-height: calc(
|
|
||||||
(<%= height || '50mm' %> * <%= scale || '1' %>) + 100px
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewContainer {
|
.previewWrapper {
|
||||||
transform: scale(<%= scale || '1' %>);
|
width: <%= (width * scale) + 'mm' || '50mm' %>;
|
||||||
min-width: calc((<%= width || '50mm' %> + 100px) * <%= scale || '1' %>);
|
height: <%= (height * scale) + 'mm' || '50mm' %>;
|
||||||
min-height: calc(
|
|
||||||
(<%= height || '50mm' %> + 100px) * <%= scale || '1' %>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
.previewDocument {
|
.previewDocument {
|
||||||
width: <%= width || '50mm' %>;
|
width: <%= (width) + 'mm' || '50mm' %>;
|
||||||
height: <%= height || '50mm' %>;
|
height: <%= (height) + 'mm' || '50mm' %>;
|
||||||
|
transform: scale(<%= scale || '1' %>);
|
||||||
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
.renderDocument {
|
.renderDocument {
|
||||||
width: <%= width || '50mm' %>;
|
width: <%= (width * scale) + 'mm' || '50mm' %>;
|
||||||
height: <%= height || '50mm' %>;
|
height: <%= (height * scale) + 'mm' || '50mm' %>;
|
||||||
transform: scale(<%= scale || '1' %>);
|
transform: scale(<%= scale || '1' %>);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -45,5 +40,10 @@
|
|||||||
<script>
|
<script>
|
||||||
JsBarcode('.documentBarcode').init();
|
JsBarcode('.documentBarcode').init();
|
||||||
</script>
|
</script>
|
||||||
|
<% if (typeof previewPaginationScript !== 'undefined' && previewPaginationScript) { %>
|
||||||
|
<script>
|
||||||
|
<%- previewPaginationScript %>
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
<div class="previewContainer">
|
<div class="previewContainer">
|
||||||
<div class="previewDocument"><%- content %></div>
|
<div class="previewWrapper">
|
||||||
|
<div class="previewDocument" id="content"><%- content %></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
src/templates/pdffactory.js
Normal file
55
src/templates/pdffactory.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import log4js from 'log4js';
|
||||||
|
import { loadConfig } from '../config.js';
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const logger = log4js.getLogger('PDF Factory');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a PDF from HTML content using Puppeteer
|
||||||
|
* @param {string} html - The HTML content to convert to PDF
|
||||||
|
* @param {Object} options - PDF generation options
|
||||||
|
* @param {number} options.width - Document width in mm
|
||||||
|
* @param {number} options.height - Document height in mm
|
||||||
|
* @returns {Promise<Buffer>} The PDF buffer
|
||||||
|
*/
|
||||||
|
export async function generatePDF(html, options = {}) {
|
||||||
|
try {
|
||||||
|
// Dynamically import puppeteer to handle cases where it might not be installed
|
||||||
|
const puppeteer = await import('puppeteer');
|
||||||
|
|
||||||
|
const browser = await puppeteer.default.launch({
|
||||||
|
headless: true,
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Set content with HTML
|
||||||
|
await page.setContent(html, {
|
||||||
|
waitUntil: 'networkidle0'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate PDF with specified dimensions
|
||||||
|
const pdfBuffer = await page.pdf({
|
||||||
|
format: options.format || undefined,
|
||||||
|
width: options.width ? `${options.width}mm` : undefined,
|
||||||
|
height: options.height ? `${options.height}mm` : undefined,
|
||||||
|
printBackground: true,
|
||||||
|
preferCSSPageSize: true,
|
||||||
|
margin: {
|
||||||
|
top: '0mm',
|
||||||
|
right: '0mm',
|
||||||
|
bottom: '0mm',
|
||||||
|
left: '0mm'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return pdfBuffer;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error generating PDF:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,9 @@ import utc from 'dayjs/plugin/utc.js';
|
|||||||
import timezone from 'dayjs/plugin/timezone.js';
|
import timezone from 'dayjs/plugin/timezone.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { getObject } from '../database/database.js';
|
import { getObject, listObjects } from '../database/database.js';
|
||||||
|
import { getModelByName } from '../utils.js';
|
||||||
|
import { generatePDF } from './pdffactory.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
@ -28,7 +30,9 @@ logger.level = config.server.logLevel;
|
|||||||
let baseTemplate;
|
let baseTemplate;
|
||||||
let baseCSS;
|
let baseCSS;
|
||||||
let previewTemplate;
|
let previewTemplate;
|
||||||
|
let renderTemplateEjs;
|
||||||
let contentPlaceholder;
|
let contentPlaceholder;
|
||||||
|
let previewPaginationScript;
|
||||||
|
|
||||||
async function loadTemplates() {
|
async function loadTemplates() {
|
||||||
// Synchronously load files
|
// Synchronously load files
|
||||||
@ -41,10 +45,18 @@ async function loadTemplates() {
|
|||||||
join(__dirname, '/assets/previewtemplate.ejs'),
|
join(__dirname, '/assets/previewtemplate.ejs'),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
renderTemplateEjs = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/rendertemplate.ejs'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
contentPlaceholder = fs.readFileSync(
|
contentPlaceholder = fs.readFileSync(
|
||||||
join(__dirname, '/assets/contentplaceholder.ejs'),
|
join(__dirname, '/assets/contentplaceholder.ejs'),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
previewPaginationScript = fs.readFileSync(
|
||||||
|
join(__dirname, '/assets/previewpagination.js'),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTemplates();
|
loadTemplates();
|
||||||
@ -60,6 +72,12 @@ function getNodeStyles(attributes) {
|
|||||||
if (attributes?.height) {
|
if (attributes?.height) {
|
||||||
styles += `height: ${attributes.height};`;
|
styles += `height: ${attributes.height};`;
|
||||||
}
|
}
|
||||||
|
if (attributes?.maxWidth) {
|
||||||
|
styles += `max-width: ${attributes.maxWidth};`;
|
||||||
|
}
|
||||||
|
if (attributes?.maxHeight) {
|
||||||
|
styles += `max-height: ${attributes.maxHeight};`;
|
||||||
|
}
|
||||||
if (attributes?.gap && attributes?.vertical != 'true') {
|
if (attributes?.gap && attributes?.vertical != 'true') {
|
||||||
styles += `column-gap: ${attributes.gap};`;
|
styles += `column-gap: ${attributes.gap};`;
|
||||||
}
|
}
|
||||||
@ -96,6 +114,15 @@ function getNodeStyles(attributes) {
|
|||||||
if (attributes?.scale) {
|
if (attributes?.scale) {
|
||||||
styles += `transform: scale(${attributes.scale});`;
|
styles += `transform: scale(${attributes.scale});`;
|
||||||
}
|
}
|
||||||
|
if (attributes?.textAlign) {
|
||||||
|
styles += `text-align: ${attributes.textAlign};`;
|
||||||
|
}
|
||||||
|
if (attributes?.textSize) {
|
||||||
|
styles += `font-size: ${attributes.textSize};`;
|
||||||
|
}
|
||||||
|
if (attributes?.wordWrap) {
|
||||||
|
styles += `word-wrap: ${attributes.wordWrap};`;
|
||||||
|
}
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +179,9 @@ async function transformCustomElements(content) {
|
|||||||
class: 'documentBarcode',
|
class: 'documentBarcode',
|
||||||
'jsbarcode-displayValue': 'false',
|
'jsbarcode-displayValue': 'false',
|
||||||
'jsbarcode-value': node.content[0],
|
'jsbarcode-value': node.content[0],
|
||||||
'jsbarcode-format': node.attrs.format
|
'jsbarcode-format': node.attrs.format,
|
||||||
|
'jsbarcode-width': node.attrs.barcodeWidth,
|
||||||
|
'jsbarcode-margin': 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -193,6 +222,27 @@ async function transformCustomElements(content) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'ProgressBar' }, node => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'div',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentProgressBar',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
tag: 'div',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentProgressBarInner',
|
||||||
|
style: `width: ${Math.round((node.content[0] || 0) * 100)}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
tree =>
|
tree =>
|
||||||
tree.match({ tag: 'DateTime' }, node => {
|
tree.match({ tag: 'DateTime' }, node => {
|
||||||
const dateTime = dayjs.utc(node.content[0]);
|
const dateTime = dayjs.utc(node.content[0]);
|
||||||
@ -204,6 +254,91 @@ async function transformCustomElements(content) {
|
|||||||
style: getNodeStyles(node.attrs)
|
style: getNodeStyles(node.attrs)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Table' }, node => {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'table',
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTable',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
tree =>
|
||||||
|
tree.match({ tag: 'Row' }, node => {
|
||||||
|
const rowType = node.attrs?.type?.toLowerCase() || '';
|
||||||
|
|
||||||
|
// Transform Col children based on the row type (header/footer/body)
|
||||||
|
const transformCols = content => {
|
||||||
|
if (!Array.isArray(content)) return content;
|
||||||
|
return content.map(child => {
|
||||||
|
if (typeof child === 'string' || child == null) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (child.tag !== 'Col') {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseAttrs = {
|
||||||
|
...child.attrs,
|
||||||
|
style: getNodeStyles(child.attrs)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rowType === 'header') {
|
||||||
|
// Header row columns become table headers
|
||||||
|
return {
|
||||||
|
...child,
|
||||||
|
tag: 'th',
|
||||||
|
attrs: baseAttrs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer and body rows both use <td>; footer is distinguished by the row class
|
||||||
|
return {
|
||||||
|
...child,
|
||||||
|
tag: 'td',
|
||||||
|
attrs: baseAttrs
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = transformCols(node.content);
|
||||||
|
|
||||||
|
if (rowType === 'header') {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'tr',
|
||||||
|
content,
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTableRowHeader',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowType === 'footer') {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'tr',
|
||||||
|
content,
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTableRowFooter',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
tag: 'tr',
|
||||||
|
content,
|
||||||
|
attrs: {
|
||||||
|
class: 'documentTableRow',
|
||||||
|
style: getNodeStyles(node.attrs)
|
||||||
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
]).process(content);
|
]).process(content);
|
||||||
|
|
||||||
@ -211,6 +346,13 @@ async function transformCustomElements(content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TemplateManager {
|
export class TemplateManager {
|
||||||
|
constructor() {
|
||||||
|
this.fc = {
|
||||||
|
listObjects: this.listObjects.bind(this),
|
||||||
|
getObject: this.getObject.bind(this),
|
||||||
|
formatDate: this.formatDate.bind(this)
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Previews an EJS template by rendering it with provided data
|
* Previews an EJS template by rendering it with provided data
|
||||||
* @param {string} templateString - The EJS template as a string
|
* @param {string} templateString - The EJS template as a string
|
||||||
@ -218,7 +360,14 @@ export class TemplateManager {
|
|||||||
* @param {Object} options - EJS rendering options
|
* @param {Object} options - EJS rendering options
|
||||||
* @returns {Promise<string>} The rendered HTML string
|
* @returns {Promise<string>} The rendered HTML string
|
||||||
*/
|
*/
|
||||||
async renderTemplate(id, content, data = {}, scale, options = {}) {
|
async renderTemplate(
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
data = {},
|
||||||
|
scale = 1,
|
||||||
|
options = {},
|
||||||
|
preview = true
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Set default options for EJS rendering
|
// Set default options for EJS rendering
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
@ -242,11 +391,28 @@ export class TemplateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const documentSize = documentTemplate.documentSize;
|
const documentSize = documentTemplate.documentSize;
|
||||||
|
if (documentSize == null) {
|
||||||
|
return { error: 'Document template size not found.' };
|
||||||
|
}
|
||||||
|
|
||||||
var templateData = data;
|
// Validate content parameter
|
||||||
|
if (content == null || typeof content !== 'string') {
|
||||||
|
return { error: 'Template content is required and must be a string.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure data has default undefefined values and then merge with data
|
||||||
|
var templateData = {};
|
||||||
if (documentTemplate.global == true) {
|
if (documentTemplate.global == true) {
|
||||||
templateData = { content: contentPlaceholder };
|
templateData = { content: contentPlaceholder, fc: this.fc };
|
||||||
|
} else {
|
||||||
|
const objectType = documentTemplate?.objectType;
|
||||||
|
const model = getModelByName(objectType);
|
||||||
|
const defaultKeys = Object.keys(model.schema.obj);
|
||||||
|
const defaultValues = {};
|
||||||
|
for (const key of defaultKeys) {
|
||||||
|
defaultValues[key] = null;
|
||||||
|
}
|
||||||
|
templateData = { ...defaultValues, ...data, fc: this.fc };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the template
|
// Render the template
|
||||||
@ -259,39 +425,79 @@ export class TemplateManager {
|
|||||||
var templateWithParentContent;
|
var templateWithParentContent;
|
||||||
|
|
||||||
if (documentTemplate.parent != undefined) {
|
if (documentTemplate.parent != undefined) {
|
||||||
|
// Validate parent content
|
||||||
|
if (
|
||||||
|
documentTemplate.parent.content == null ||
|
||||||
|
typeof documentTemplate.parent.content !== 'string'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
error: 'Parent template content is required and must be a string.'
|
||||||
|
};
|
||||||
|
}
|
||||||
templateWithParentContent = await ejs.render(
|
templateWithParentContent = await ejs.render(
|
||||||
documentTemplate.parent.content,
|
documentTemplate.parent.content,
|
||||||
{ content: templateContent },
|
{ content: templateContent, fc: this.fc },
|
||||||
defaultOptions
|
defaultOptions
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
templateWithParentContent = templateContent;
|
templateWithParentContent = templateContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate rendered content before transformation
|
||||||
|
if (
|
||||||
|
templateWithParentContent == null ||
|
||||||
|
typeof templateWithParentContent !== 'string'
|
||||||
|
) {
|
||||||
|
return { error: 'Failed to render template content.' };
|
||||||
|
}
|
||||||
|
|
||||||
const templateHtml = await transformCustomElements(
|
const templateHtml = await transformCustomElements(
|
||||||
templateWithParentContent
|
templateWithParentContent
|
||||||
);
|
);
|
||||||
|
|
||||||
const previewHtml = await ejs.render(
|
// Validate transformed HTML
|
||||||
previewTemplate,
|
if (templateHtml == null || typeof templateHtml !== 'string') {
|
||||||
{ content: templateHtml },
|
return { error: 'Failed to transform template content.' };
|
||||||
defaultOptions
|
}
|
||||||
);
|
|
||||||
|
var innerHtml = null;
|
||||||
|
|
||||||
|
if (preview == true) {
|
||||||
|
innerHtml = await ejs.render(
|
||||||
|
previewTemplate,
|
||||||
|
{ content: templateHtml },
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
innerHtml = await ejs.render(
|
||||||
|
renderTemplateEjs,
|
||||||
|
{ content: templateHtml },
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate inner HTML
|
||||||
|
if (innerHtml == null || typeof innerHtml !== 'string') {
|
||||||
|
return { error: 'Failed to render inner template content.' };
|
||||||
|
}
|
||||||
|
|
||||||
const baseHtml = await ejs.render(
|
const baseHtml = await ejs.render(
|
||||||
baseTemplate,
|
baseTemplate,
|
||||||
{
|
{
|
||||||
content: previewHtml,
|
content: innerHtml,
|
||||||
width: `${documentSize.width}mm`,
|
width: documentSize.width,
|
||||||
height: `${documentSize.height}mm`,
|
height: documentSize.height,
|
||||||
scale: `${scale}`,
|
scale: `${scale}`,
|
||||||
baseCSS: baseCSS
|
baseCSS: baseCSS,
|
||||||
|
previewPaginationScript: preview ? previewPaginationScript : ''
|
||||||
},
|
},
|
||||||
defaultOptions
|
defaultOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const previewObject = {
|
const previewObject = {
|
||||||
html: baseHtml
|
html: baseHtml,
|
||||||
|
width: documentSize.width,
|
||||||
|
height: documentSize.height
|
||||||
};
|
};
|
||||||
|
|
||||||
return previewObject;
|
return previewObject;
|
||||||
@ -315,4 +521,73 @@ export class TemplateManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a template to PDF format
|
||||||
|
* @param {string} id - The document template ID
|
||||||
|
* @param {string} content - The template content
|
||||||
|
* @param {Object} data - Data object to pass to the template
|
||||||
|
* @param {number} scale - Scale factor for rendering
|
||||||
|
* @param {Object} options - EJS rendering options
|
||||||
|
* @returns {Promise<Object>} Object containing PDF buffer or error
|
||||||
|
*/
|
||||||
|
async renderPDF(id, content, data = {}, options = {}) {
|
||||||
|
try {
|
||||||
|
logger.debug('Rendering PDF for template:', id);
|
||||||
|
|
||||||
|
const renderedTemplate = await this.renderTemplate(
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
data,
|
||||||
|
1,
|
||||||
|
options,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (renderedTemplate.error != undefined) {
|
||||||
|
return { error: renderedTemplate.error };
|
||||||
|
}
|
||||||
|
const baseHtml = renderedTemplate.html;
|
||||||
|
|
||||||
|
// Generate PDF using PDF factory
|
||||||
|
const pdfBuffer = await generatePDF(baseHtml, {
|
||||||
|
width: renderedTemplate.width,
|
||||||
|
height: renderedTemplate.height
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfObject = {
|
||||||
|
pdf: pdfBuffer
|
||||||
|
};
|
||||||
|
return pdfObject;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error whilst rendering PDF:', error.message);
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listObjects(objectType, filter = {}, populate = []) {
|
||||||
|
const model = getModelByName(objectType);
|
||||||
|
if (model == undefined) {
|
||||||
|
throw new Error('Farm Control: Object type not found.');
|
||||||
|
}
|
||||||
|
const objects = await listObjects({
|
||||||
|
model,
|
||||||
|
filter,
|
||||||
|
populate
|
||||||
|
});
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date, format) {
|
||||||
|
return dayjs(date).format(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getObject(objectType, id) {
|
||||||
|
const model = getModelByName(objectType);
|
||||||
|
if (model == undefined) {
|
||||||
|
throw new Error('Farm Control: Object type not found.');
|
||||||
|
}
|
||||||
|
const object = await getObject({ model, id, cached: true });
|
||||||
|
return object;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user