Updated info.js for new printers key. Updated print.js to forward ESC/POS printer IPs. Updated keys within variables.js. Added 'UI_BACK_BUTTON', 'PRINTER_TYPE' and 'PRINTER_IP' to the deprecated strings array.js. Updated status.js with new printing module. Updated docker-compose.yml environment variables. Updated dependencies. Hotfix body type for express v5. Updated README.md. Removed back button from navigation.ejs. Updated print.ejs and bulk-print.ejs with printer selection. Updated print logic within server.js to allow printer selection by user

This commit is contained in:
Glenn de Haan
2025-05-19 18:50:46 +02:00
parent 8f12d4de17
commit 5b2f8a1b30
14 changed files with 669 additions and 438 deletions

View File

@@ -6,7 +6,7 @@ UniFi Voucher Site is a web-based platform for generating and managing UniFi net
![Vouchers Overview - Desktop](.docs/images/desktop_1.png)
> Upgrading from 4.x to 5.x? Please take a look at the [migration guide](#migration-from-4x-to-5x)
> Upgrading from 5.x to 6.x? Please take a look at the [migration guide](#migration-from-5x-to-6x)
## Features
@@ -108,10 +108,8 @@ services:
SERVICE_WEB: 'true'
# Enable/disable the API
SERVICE_API: 'false'
# Enable/disable the printer and set the preferred type, currently supported types: pdf, escpos
PRINTER_TYPE: ''
# IP address to your network enabled ESC/POS compatible printer (Only required when using PRINTER_TYPE: 'escpos')
PRINTER_IP: '192.168.1.1'
# Enable/disable the printers and setup available printers, currently supported: pdf,escpos ip (Example: pdf,192.168.1.10)
PRINTERS: ''
# SMTP Mail from email address (optional)
SMTP_FROM: ''
# SMTP Mail server hostname/ip (optional)
@@ -135,8 +133,6 @@ services:
TRANSLATION_DEFAULT: 'en'
# Enables/disables translation debugging, when enabled only translation keys are shown
TRANSLATION_DEBUG: 'false'
# Enables/disables a back-button next to the logo to go back 1 page in history (Could be used with multi-page kiosks)
UI_BACK_BUTTON: 'false'
```
### Home Assistant Add-on
@@ -468,17 +464,20 @@ The print functionality is compatible with most 80mm thermal receipt printers co
To enable the print feature, you need to set the following environment variables:
```env
PRINTER_TYPE: ''
PRINTER_IP: ''
PRINTERS: ''
```
Heres what each variable represents:
- **`PRINTER_TYPE`**: Sets the printer type used by UniFi Voucher Site. Supported options:
- **`PRINTERS`**: Sets the printer type used by UniFi Voucher Site. Supported options:
- `pdf`: For generating PDF files formatted for 80mm paper width.
- `escpos`: For printing directly to network-enabled ESC/POS compatible printers.
- `escpos`: For printing directly to network-enabled ESC/POS compatible printers. Specify the IP address of the network-enabled ESC/POS printer
- **`PRINTER_IP`**: Specifies the IP address of the network-enabled ESC/POS printer. This variable is only required when `PRINTER_TYPE` is set to `escpos`.
> You can have multiple printers available at the same time.
> Let's say you have 2 ESC/POS network printers and want to print via pdf, then define:
> ```env
> PRINTERS: 'pdf,192.168.1.10,192.168.1.11'
> ```
### Usage
@@ -494,7 +493,7 @@ The application will automatically format the voucher for 80mm paper width, ensu
#### ESC/POS
For network-enabled ESC/POS compatible printers, set the `PRINTER_TYPE` to `escpos` and provide the printer's IP address in the `PRINTER_IP` variable. Once configured, you can print vouchers directly to your network printer from the UniFi Voucher Site application.
For network-enabled ESC/POS compatible printers, provide the printer's IP address in the `PRINTERS` variable. Once configured, you can print vouchers directly to your network printer from the UniFi Voucher Site application.
Just like with PDF printing, navigate to the voucher and click on the "Print" button. The application will send the print job directly to the ESC/POS printer over the network, ensuring quick and seamless voucher printing. Make sure your printer supports ESC/POS commands and is correctly configured to accept print jobs over the network.
@@ -647,6 +646,37 @@ Detailed information on the changes in each release can be found on the [GitHub
## Migration Guide
### Migration from 5.x to 6.x
When upgrading from 5.x to 6.x, the following changes need to be made:
1. **`UI_BACK_BUTTON` Removed**
- The `UI_BACK_BUTTON` configuration option has been **removed** in 6.x.
- This setting is no longer used and can be safely **removed from your environment configuration**.
2. **Printer Configuration Changes**
- The legacy printer configuration options **`PRINTER_TYPE`** and **`PRINTER_IP`** have been replaced by a single setting: **`PRINTERS`**.
- The new format is a **comma-separated string** combining the printer type and IP address.
**Before (5.x):**
```env
PRINTER_TYPE='pdf'
PRINTER_IP='192.168.1.10'
```
**After (6.x):**
```env
PRINTERS='pdf,192.168.1.10'
```
- Update your configuration to use `PRINTERS` and remove the old `PRINTER_TYPE` and `PRINTER_IP` variables.
> Make sure to clean up any deprecated variables and update your printer configuration to ensure compatibility with 6.x.
### Migration from 4.x to 5.x
When upgrading from 4.x to 5.x, the following changes need to be made:

View File

@@ -24,8 +24,7 @@ services:
VOUCHER_CUSTOM: 'true'
SERVICE_WEB: 'true'
SERVICE_API: 'false'
PRINTER_TYPE: ''
PRINTER_IP: '192.168.1.1'
PRINTERS: ''
SMTP_FROM: ''
SMTP_HOST: ''
SMTP_PORT: ''

View File

@@ -95,7 +95,7 @@ module.exports = () => {
/**
* Log printer status
*/
log.info(`[Printer] ${variables.printerType !== '' ? `Enabled! Type: ${variables.printerType}${variables.printerType === 'escpos' ? `, IP: ${variables.printerIp}` : ''}` : 'Disabled!'}`);
log.info(`[Printers] ${variables.printers !== '' ? `Enabled! Available: ${variables.printers.split(',').join(', ')}` : 'Disabled!'}`);
/**
* Log email status

View File

@@ -225,16 +225,17 @@ module.exports = {
*
* @param voucher
* @param language
* @param ip
* @return {Promise<unknown>}
*/
escpos: (voucher, language) => {
escpos: (voucher, language, ip) => {
return new Promise(async (resolve, reject) => {
// Create new translator
const t = translation('print', language);
const printer = new ThermalPrinter({
type: PrinterTypes.EPSON,
interface: `tcp://${variables.printerIp}`
interface: `tcp://${ip}`
});
const status = await printer.isPrinterConnected();

View File

@@ -32,8 +32,7 @@ module.exports = {
authOidcClientId: config('auth_oidc_client_id') || process.env.AUTH_OIDC_CLIENT_ID || '',
authOidcClientSecret: config('auth_oidc_client_secret') || process.env.AUTH_OIDC_CLIENT_SECRET || '',
authDisabled: config('auth_disable') || (process.env.AUTH_DISABLE === 'true') || false,
printerType: config('printer_type') || process.env.PRINTER_TYPE || '',
printerIp: config('printer_ip') || process.env.PRINTER_IP || '192.168.1.1',
printers: config('printers') || process.env.PRINTERS || '',
smtpFrom: config('smtp_from') || process.env.SMTP_FROM || '',
smtpHost: config('smtp_host') || process.env.SMTP_HOST || '',
smtpPort: config('smtp_port') || process.env.SMTP_PORT || 25,
@@ -45,7 +44,6 @@ module.exports = {
logLevel: config('log_level') || process.env.LOG_LEVEL || 'info',
translationDefault: config('translation_default') || process.env.TRANSLATION_DEFAULT || 'en',
translationDebug: config('translation_debug') || (process.env.TRANSLATION_DEBUG === 'true') || false,
uiBackButton: config('ui_back_button') || (process.env.UI_BACK_BUTTON === 'true') || false,
gitTag: process.env.GIT_TAG || 'master',
gitBuild: fs.existsSync('/etc/unifi_voucher_site_build') ? fs.readFileSync('/etc/unifi_voucher_site_build', 'utf-8') : 'Development'
};

903
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"dependencies": {
"cookie-parser": "^1.4.7",
"ejs": "^3.1.10",
"express": "^4.21.2",
"express": "^5.1.0",
"express-locale": "^2.0.2",
"express-openid-connect": "^2.18.1",
"js-logger": "^1.6.1",
@@ -36,8 +36,8 @@
"qrcode": "^1.5.4"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.5",
"@tailwindcss/cli": "^4.1.7",
"@tailwindcss/forms": "^0.5.10",
"tailwindcss": "^4.1.5"
"tailwindcss": "^4.1.7"
}
}

View File

@@ -165,7 +165,7 @@ if(variables.serviceWeb) {
}
// Check if we need to generate a voucher or send an email with an existing voucher
if(req.body.id && req.body.code && req.body.email) {
if(req.body && req.body.id && req.body.code && req.body.email) {
// Check if email functions are enabled
if(variables.smtpFrom === '' || variables.smtpHost === '' || variables.smtpPort === '') {
res.status(501).send();
@@ -363,7 +363,7 @@ if(variables.serviceWeb) {
}
});
app.get('/voucher/:id/print', [authorization.web], async (req, res) => {
if(variables.printerType === '') {
if(variables.printers === '') {
res.status(501).send();
return;
}
@@ -377,6 +377,7 @@ if(variables.serviceWeb) {
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
languages,
defaultLanguage: variables.translationDefault,
printers: variables.printers.split(','),
voucher,
updated: cache.updated
});
@@ -388,17 +389,22 @@ if(variables.serviceWeb) {
}
});
app.post('/voucher/:id/print', [authorization.web], async (req, res) => {
if(variables.printerType === '') {
if(variables.printers === '') {
res.status(501).send();
return;
}
if(!variables.printers.includes(req.body.printer)) {
res.status(400).send();
return;
}
const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id;
});
if(voucher) {
if(variables.printerType === 'pdf') {
if(req.body.printer === 'pdf') {
const buffers = await print.pdf(voucher, req.body.language);
const pdfData = Buffer.concat(buffers);
res.writeHead(200, {
@@ -406,10 +412,8 @@ if(variables.serviceWeb) {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment;filename=voucher_${req.params.id}.pdf`
}).end(pdfData);
}
if(variables.printerType === 'escpos') {
const printResult = await print.escpos(voucher, req.body.language).catch((e) => {
} else {
const printResult = await print.escpos(voucher, req.body.language, req.body.printer).catch((e) => {
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
});
@@ -525,12 +529,11 @@ if(variables.serviceWeb) {
info_text: req.flashMessage.message || '',
error: req.flashMessage.type === 'error',
error_text: req.flashMessage.message || '',
uiBackButton: variables.uiBackButton,
kioskEnabled: variables.kioskEnabled,
timeConvert: time,
bytesConvert: bytes,
email_enabled: variables.smtpFrom !== '' && variables.smtpHost !== '' && variables.smtpPort !== '',
printer_enabled: variables.printerType !== '',
printer_enabled: variables.printers !== '',
voucher_types: types(variables.voucherTypes),
voucher_custom: variables.voucherCustom,
vouchers: cache.vouchers.filter((item) => {
@@ -617,7 +620,6 @@ if(variables.serviceWeb) {
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
gitTag: variables.gitTag,
gitBuild: variables.gitBuild,
uiBackButton: variables.uiBackButton,
kioskEnabled: variables.kioskEnabled,
user: user,
userIcon: req.oidc ? crypto.createHash('sha256').update(user.email).digest('hex') : '',
@@ -626,7 +628,7 @@ if(variables.serviceWeb) {
});
});
app.get('/bulk/print', [authorization.web], async (req, res) => {
if(variables.printerType === '') {
if(variables.printers === '') {
res.status(501).send();
return;
}
@@ -637,16 +639,22 @@ if(variables.serviceWeb) {
bytesConvert: bytes,
languages,
defaultLanguage: variables.translationDefault,
printers: variables.printers.split(','),
vouchers: cache.vouchers,
updated: cache.updated
});
});
app.post('/bulk/print', [authorization.web], async (req, res) => {
if(variables.printerType === '') {
if(variables.printers === '') {
res.status(501).send();
return;
}
if(!variables.printers.includes(req.body.printer)) {
res.status(400).send();
return;
}
if(!req.body.vouchers) {
res.cookie('flashMessage', JSON.stringify({type: 'error', message: 'No selected vouchers to print!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
return;
@@ -664,7 +672,7 @@ if(variables.serviceWeb) {
});
if(!vouchers.includes(undefined)) {
if(variables.printerType === 'pdf') {
if(req.body.printer === 'pdf') {
const buffers = await print.pdf(vouchers, req.body.language, true);
const pdfData = Buffer.concat(buffers);
res.writeHead(200, {
@@ -672,13 +680,11 @@ if(variables.serviceWeb) {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment;filename=bulk_vouchers_${new Date().getTime()}.pdf`
}).end(pdfData);
}
if(variables.printerType === 'escpos') {
} else {
let printSuccess = true;
for(let voucher = 0; voucher < vouchers.length; voucher++) {
const printResult = await print.escpos(vouchers[voucher], req.body.language).catch((e) => {
const printResult = await print.escpos(vouchers[voucher], req.body.language, req.body.printer).catch((e) => {
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
});

View File

@@ -18,6 +18,16 @@
<div class="flex flex-1 flex-col justify-between">
<div class="divide-y divide-black/5 dark:divide-white/5 px-4 sm:px-6">
<div class="space-y-6 pb-5 pt-6">
<div>
<label for="printer" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Printer</label>
<div class="mt-2">
<select id="printer" name="printer" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
<% printers.forEach((printer) => { %>
<option value="<%= printer %>"><%= printer %> (<%= printer === 'pdf' ? 'PDF' : 'ESC/POS' %>)</option>
<% }); %>
</select>
</div>
</div>
<div>
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
<div class="mt-2">

View File

@@ -18,6 +18,16 @@
<div class="flex flex-1 flex-col justify-between">
<div class="divide-y divide-black/5 dark:divide-white/5 px-4 sm:px-6">
<div class="space-y-6 pb-5 pt-6">
<div>
<label for="printer" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Printer</label>
<div class="mt-2">
<select id="printer" name="printer" class="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 dark:text-white dark:bg-white/5 ring-1 ring-inset ring-gray-300 dark:ring-white/10 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6 **:text-black">
<% printers.forEach((printer) => { %>
<option value="<%= printer %>"><%= printer %> (<%= printer === 'pdf' ? 'PDF' : 'ESC/POS' %>)</option>
<% }); %>
</select>
</div>
</div>
<div>
<label for="language" class="block text-sm font-medium leading-6 text-gray-900 dark:text-white">Language</label>
<div class="mt-2">

View File

@@ -2,13 +2,6 @@
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
<% if(uiBackButton) { %>
<button aria-label="Back to Previous Page" onclick="window.history.go(-1);">
<svg xmlns="http://www.w3.org/2000/svg" class="text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-100 w-10 h-10 mr-4" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-4.28 9.22a.75.75 0 0 0 0 1.06l3 3a.75.75 0 1 0 1.06-1.06l-1.72-1.72h5.69a.75.75 0 0 0 0-1.5h-5.69l1.72-1.72a.75.75 0 0 0-1.06-1.06l-3 3Z" clip-rule="evenodd" />
</svg>
</button>
<% } %>
<a href="<%= baseUrl %>/vouchers" class="flex shrink-0 items-center">
<img class="h-12 w-auto" width="48" height="48" alt="UniFi Voucher Site Logo" src="<%= baseUrl %>/images/logo.png">
<div class="hidden sm:block ml-4 text-2xl font-semibold leading-7 text-gray-900 dark:text-white">

View File

@@ -128,35 +128,6 @@
<%= status.printing.details %>&nbsp;<a href="<%= status.printing.info %>" class="italic text-xs underline" aria-label="More Info Link" target="_blank" rel="noreferrer noopener">More Info</a>
</td>
</tr>
<tr class="border-b border-gray-300 dark:border-white/10 bg-white dark:bg-white/5">
<td class="p-4 align-middle font-medium flex items-center gap-2 pl-6">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625Z" />
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
</svg>
PDF
</td>
<td class="p-4 align-middle">
<%- include('partials/tag', {status: status.printing.modules.pdf.status}) %>
</td>
<td class="p-4 align-middle">
<%= status.printing.modules.pdf.details %>&nbsp;<a href="<%= status.printing.modules.pdf.info %>" class="italic text-xs underline" aria-label="More Info Link" target="_blank" rel="noreferrer noopener">More Info</a>
</td>
</tr>
<tr class="border-b border-gray-300 dark:border-white/10 bg-white dark:bg-white/5">
<td class="p-4 align-middle font-medium flex items-center gap-2 pl-6">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path fill-rule="evenodd" d="M7.875 1.5C6.839 1.5 6 2.34 6 3.375v2.99c-.426.053-.851.11-1.274.174-1.454.218-2.476 1.483-2.476 2.917v6.294a3 3 0 0 0 3 3h.27l-.155 1.705A1.875 1.875 0 0 0 7.232 22.5h9.536a1.875 1.875 0 0 0 1.867-2.045l-.155-1.705h.27a3 3 0 0 0 3-3V9.456c0-1.434-1.022-2.7-2.476-2.917A48.716 48.716 0 0 0 18 6.366V3.375c0-1.036-.84-1.875-1.875-1.875h-8.25ZM16.5 6.205v-2.83A.375.375 0 0 0 16.125 3h-8.25a.375.375 0 0 0-.375.375v2.83a49.353 49.353 0 0 1 9 0Zm-.217 8.265c.178.018.317.16.333.337l.526 5.784a.375.375 0 0 1-.374.409H7.232a.375.375 0 0 1-.374-.409l.526-5.784a.373.373 0 0 1 .333-.337 41.741 41.741 0 0 1 8.566 0Zm.967-3.97a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75H18a.75.75 0 0 1-.75-.75V10.5ZM15 9.75a.75.75 0 0 0-.75.75v.008c0 .414.336.75.75.75h.008a.75.75 0 0 0 .75-.75V10.5a.75.75 0 0 0-.75-.75H15Z" clip-rule="evenodd" />
</svg>
ESC/POS
</td>
<td class="p-4 align-middle">
<%- include('partials/tag', {status: status.printing.modules.escpos.status}) %>
</td>
<td class="p-4 align-middle">
<%= status.printing.modules.escpos.details %>&nbsp;<a href="<%= status.printing.modules.escpos.info %>" class="italic text-xs underline" aria-label="More Info Link" target="_blank" rel="noreferrer noopener">More Info</a>
</td>
</tr>
<tr class="border-b border-gray-300 dark:border-white/10 bg-white dark:bg-white/5">
<td class="p-4 align-middle font-medium flex items-center gap-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">

View File

@@ -7,6 +7,9 @@ module.exports = {
'DISABLE_AUTH',
'AUTH_OIDC_CLIENT_TYPE',
'AUTH_PASSWORD',
'AUTH_TOKEN'
'AUTH_TOKEN',
'UI_BACK_BUTTON',
'PRINTER_TYPE',
'PRINTER_IP'
]
};

View File

@@ -47,29 +47,12 @@ module.exports = () => {
},
printing: {
status: {
text: variables.printerType !== '' ? 'Enabled' : 'Disabled',
state: variables.printerType !== '' ? 'green' : 'red'
text: variables.printers !== '' ? 'Enabled' : 'Disabled',
state: variables.printers !== '' ? 'green' : 'red'
},
details: variables.printerType !== '' ? 'Printing service has been configured.' : 'No printing service enabled.',
details: variables.printers !== '' ? `Printing service has been configured. Available printers: ${variables.printers.split(',').join(', ')}` : 'No printing service enabled.',
info: 'https://github.com/glenndehaan/unifi-voucher-site#print-functionality',
modules: {
pdf: {
status: {
text: variables.printerType === 'pdf' ? 'Enabled' : 'Disabled',
state: variables.printerType === 'pdf' ? 'green' : 'red'
},
details: variables.printerType === 'pdf' ? 'PDF Service enabled.' : 'PDF Service not enabled.',
info: 'https://github.com/glenndehaan/unifi-voucher-site#pdf'
},
escpos: {
status: {
text: variables.printerType === 'escpos' ? 'Enabled' : 'Disabled',
state: variables.printerType === 'escpos' ? 'green' : 'red'
},
details: variables.printerType === 'escpos' ? `ESC/POS Printing on ${variables.printerIp}.` : 'ESC/POS Service not enabled.',
info: 'https://github.com/glenndehaan/unifi-voucher-site#escpos'
}
}
modules: {}
},
email: {
status: {