Updated screenshots. Implemented color-scheme script to force browser color-scheme. Implemented print functionality. Updated README.md. Implemented print route.
28
README.md
@@ -4,7 +4,7 @@ A small UniFi Voucher Site for simple voucher creation
|
|||||||
|
|
||||||
[](https://hub.docker.com/r/glenndehaan/unifi-voucher-site)
|
[](https://hub.docker.com/r/glenndehaan/unifi-voucher-site)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
@@ -164,25 +164,43 @@ To install the UniFi Voucher Site add-on for Home Assistant, follow these steps:
|
|||||||
4. Once the repository is added, you will find the "UniFi Voucher Site" add-on in the add-on store. Click on it.
|
4. Once the repository is added, you will find the "UniFi Voucher Site" add-on in the add-on store. Click on it.
|
||||||
5. Click "Install" and wait for the installation to complete.
|
5. Click "Install" and wait for the installation to complete.
|
||||||
|
|
||||||
|
## Print Functionality
|
||||||
|
|
||||||
|
The UniFi Voucher Site application includes built-in support for printing vouchers using 80mm receipt printers, offering a convenient way to distribute vouchers in physical format.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
|
||||||
|
The print functionality is compatible with most 80mm thermal receipt printers commonly used in various industries. These printers typically use thermal printing technology, eliminating the need for ink cartridges and ensuring efficient and cost-effective voucher printing.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Once your 80mm receipt printer is configured and connected, you can easily print vouchers directly from the UniFi Voucher Site application. Simply navigate to the voucher within the interface and click on the "Print" button.
|
||||||
|
|
||||||
|
The application will automatically format the voucher for 80mm paper width, ensuring optimal printing results. Depending on your printer settings and preferences, you may adjust print quality, paper type, and other printing parameters to suit your needs.
|
||||||
|
|
||||||
|
### Example Print PDF
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Login (Desktop)
|
### Login (Desktop)
|
||||||

|

|
||||||
|
|
||||||
### Vouchers Overview (Desktop)
|
### Vouchers Overview (Desktop)
|
||||||

|

|
||||||
|
|
||||||
### Create Voucher (Desktop)
|
### Create Voucher (Desktop)
|
||||||

|

|
||||||
|
|
||||||
### Login (Mobile)
|
### Login (Mobile)
|
||||||

|

|
||||||
|
|
||||||
### Vouchers Overview (Mobile)
|
### Vouchers Overview (Mobile)
|
||||||

|

|
||||||
|
|
||||||
### Create Voucher (Mobile)
|
### Create Voucher (Mobile)
|
||||||

|

|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
1305
package-lock.json
generated
@@ -21,6 +21,7 @@
|
|||||||
"js-logger": "^1.6.1",
|
"js-logger": "^1.6.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-unifi": "^2.5.1",
|
"node-unifi": "^2.5.1",
|
||||||
|
"pdfkit": "^0.15.0",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 40 KiB |
113
server.js
@@ -6,6 +6,7 @@ const os = require('os');
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
|
const PDFDocument = require('pdfkit');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import own modules
|
* Import own modules
|
||||||
@@ -235,6 +236,118 @@ if(webService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
app.get('/voucher/:id/print', [authorization.web], async (req, res) => {
|
||||||
|
const voucher = cache.vouchers.find((e) => {
|
||||||
|
return e._id === req.params.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(voucher) {
|
||||||
|
const doc = new PDFDocument({
|
||||||
|
bufferPages: true,
|
||||||
|
size: [226.77165354330398, 290],
|
||||||
|
margins : {
|
||||||
|
top: 20,
|
||||||
|
bottom: 20,
|
||||||
|
left: 20,
|
||||||
|
right: 20
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buffers = [];
|
||||||
|
doc.on('data', buffers.push.bind(buffers));
|
||||||
|
doc.on('end', () => {
|
||||||
|
let pdfData = Buffer.concat(buffers);
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Length': Buffer.byteLength(pdfData),
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `attachment;filename=voucher_${req.params.id}.pdf`
|
||||||
|
}).end(pdfData);
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.image('public/images/logo_grayscale.png', 75, 15, {fit: [75, 75], align: 'center', valign: 'center'});
|
||||||
|
|
||||||
|
doc.moveDown(6);
|
||||||
|
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(20)
|
||||||
|
.text(`WiFi Voucher Code`, {
|
||||||
|
align: 'center'
|
||||||
|
});
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(15)
|
||||||
|
.text(`${voucher.code.slice(0, 5)}-${voucher.code.slice(5)}`, {
|
||||||
|
align: 'center'
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.moveDown(2);
|
||||||
|
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(12)
|
||||||
|
.text(`Voucher Details`);
|
||||||
|
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`--------------------------------------------------------`);
|
||||||
|
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`Type: `, {
|
||||||
|
continued: true
|
||||||
|
});
|
||||||
|
doc.font('Helvetica')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(voucher.quota === 0 ? 'Multi-use' : 'Single-use');
|
||||||
|
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`Duration: `, {
|
||||||
|
continued: true
|
||||||
|
});
|
||||||
|
doc.font('Helvetica')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(time(voucher.duration));
|
||||||
|
|
||||||
|
if(voucher.qos_usage_quota) {
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`Data Limit: `, {
|
||||||
|
continued: true
|
||||||
|
});
|
||||||
|
doc.font('Helvetica')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`${voucher.qos_usage_quota}MB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(voucher.qos_rate_max_down) {
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`Download Limit: `, {
|
||||||
|
continued: true
|
||||||
|
});
|
||||||
|
doc.font('Helvetica')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`${voucher.qos_rate_max_down}kbps`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(voucher.qos_rate_max_up) {
|
||||||
|
doc.font('Helvetica-Bold')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`Upload Limit: `, {
|
||||||
|
continued: true
|
||||||
|
});
|
||||||
|
doc.font('Helvetica')
|
||||||
|
.fontSize(10)
|
||||||
|
.text(`${voucher.qos_rate_max_up}kbps`);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.end();
|
||||||
|
} else {
|
||||||
|
res.status(404);
|
||||||
|
res.render('404', {
|
||||||
|
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
app.get('/vouchers', [authorization.web], async (req, res) => {
|
app.get('/vouchers', [authorization.web], async (req, res) => {
|
||||||
if(req.query.refresh) {
|
if(req.query.refresh) {
|
||||||
log.info('[Cache] Requesting UniFi Vouchers...');
|
log.info('[Cache] Requesting UniFi Vouchers...');
|
||||||
|
|||||||
@@ -43,5 +43,21 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.querySelector('html').setAttribute('style', 'color-scheme: dark;');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||||
|
const newColorScheme = event.matches ? "dark" : "light";
|
||||||
|
|
||||||
|
if(newColorScheme === 'light') {
|
||||||
|
document.querySelector('html').removeAttribute('style');
|
||||||
|
} else {
|
||||||
|
document.querySelector('html').setAttribute('style', 'color-scheme: dark;');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -70,5 +70,21 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.querySelector('html').setAttribute('style', 'color-scheme: dark;');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||||
|
const newColorScheme = event.matches ? "dark" : "light";
|
||||||
|
|
||||||
|
if(newColorScheme === 'light') {
|
||||||
|
document.querySelector('html').removeAttribute('style');
|
||||||
|
} else {
|
||||||
|
document.querySelector('html').setAttribute('style', 'color-scheme: dark;');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -183,6 +183,13 @@
|
|||||||
<path fill-rule="evenodd" d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<a href="<%= baseUrl %>/voucher/<%= voucher._id %>/print" type="button" class="relative rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white">
|
||||||
|
<span class="absolute -inset-1.5"></span>
|
||||||
|
<span class="sr-only">Print Voucher Code</span>
|
||||||
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
<a href="<%= baseUrl %>/voucher/<%= voucher._id %>/remove" type="button" class="remove-button relative rounded-full p-1 text-red-500 dark:text-red-400 hover:text-black dark:hover:text-white">
|
<a href="<%= baseUrl %>/voucher/<%= voucher._id %>/remove" type="button" class="remove-button relative rounded-full p-1 text-red-500 dark:text-red-400 hover:text-black dark:hover:text-white">
|
||||||
<span class="absolute -inset-1.5"></span>
|
<span class="absolute -inset-1.5"></span>
|
||||||
<span class="sr-only">Remove Voucher Code</span>
|
<span class="sr-only">Remove Voucher Code</span>
|
||||||
@@ -300,6 +307,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
document.querySelector('html').setAttribute('style', 'color-scheme: dark;');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
||||||
|
const newColorScheme = event.matches ? "dark" : "light";
|
||||||
|
|
||||||
|
if(newColorScheme === 'light') {
|
||||||
|
document.querySelector('html').removeAttribute('style');
|
||||||
|
} else {
|
||||||
|
document.querySelector('html').setAttribute('style', 'color-scheme: dark;');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
const createDialog = document.querySelector('#create-dialog');
|
const createDialog = document.querySelector('#create-dialog');
|
||||||
const createForum = document.querySelector("#voucher-forum");
|
const createForum = document.querySelector("#voucher-forum");
|
||||||
|
|||||||