websocket works and qr code
|
@ -0,0 +1,264 @@
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const base64url = require('base64url');
|
||||||
|
const multer = require('multer');
|
||||||
|
const { log } = require('console');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const { convertSvgToPng } = require('./utils/imageConverter');
|
||||||
|
const websocket = require('./websocket');
|
||||||
|
const translations = require('./translations');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
|
// Configure EJS
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(bodyParser.json({ limit: "50mb" }));
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// Initialize WebSocket server
|
||||||
|
const server = app.listen(80, () => {
|
||||||
|
console.log('Server running on port 80');
|
||||||
|
});
|
||||||
|
websocket.initialize(server);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Static images mapping
|
||||||
|
const staticImages = {
|
||||||
|
home: 'payment.png',
|
||||||
|
scan: 'Scanner.png',
|
||||||
|
printer: 'Check.png',
|
||||||
|
settings: 'Settings.png',
|
||||||
|
pinpad: 'pinpad.png',
|
||||||
|
cashier: 'cashier.png',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Topics list
|
||||||
|
const topics = [
|
||||||
|
{ deviceName: 'M50F_Igor', deviceID: '01534090202502210142' },
|
||||||
|
{ deviceName: 'M60_Igor', deviceID: '01620013202312220735' },
|
||||||
|
{ deviceName: 'M70_Igor', deviceID: '01723060202412010280' },
|
||||||
|
{ deviceName: 'M30_Igor', deviceID: '01364100202503060196' },
|
||||||
|
|
||||||
|
{ deviceName: 'M50F_Ahmed', deviceID: '01534090202502210065' },
|
||||||
|
{ deviceName: 'M60_Ahmed', deviceID: '01620013202312221500' },
|
||||||
|
{ deviceName: 'M30_Ahmed', deviceID: '01364100202503060115' },
|
||||||
|
{ deviceName: 'M20_Ahmed', deviceID: '01220110202305150514' },
|
||||||
|
{ deviceName: 'M70_Ahmed', deviceID: '01723060202412010160' },
|
||||||
|
|
||||||
|
{ deviceName: 'M60F-NOSN', deviceID: 'm60f_pos_no_sn' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sample configuration
|
||||||
|
const defaultConfig = {
|
||||||
|
type: "configuration",
|
||||||
|
navItems: [
|
||||||
|
{
|
||||||
|
navPage: "NAV_HOME",
|
||||||
|
navName: "Оплата",
|
||||||
|
icon: { type: "resource", value: "home" },
|
||||||
|
position: 0,
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navPage: "NAV_SCAN",
|
||||||
|
navName: "Сканнер",
|
||||||
|
icon: { type: "resource", value: "scan" },
|
||||||
|
position: 1,
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navPage: "NAV_PRINTER",
|
||||||
|
navName: "Принтер",
|
||||||
|
icon: { type: "resource", value: "printer" },
|
||||||
|
position: 2,
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navPage: "NAV_SETTINGS",
|
||||||
|
navName: "Настройки",
|
||||||
|
icon: { type: "resource", value: "settings" },
|
||||||
|
position: 3,
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
mainLogo: {
|
||||||
|
type: "base64",
|
||||||
|
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABlUAAAE "
|
||||||
|
},
|
||||||
|
footerLogo: {
|
||||||
|
type: "base64",
|
||||||
|
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaQAAAWY"
|
||||||
|
},
|
||||||
|
footerLogoVisibility: "true",
|
||||||
|
footerGreetingText: "Привет Игорь! ))",
|
||||||
|
footerText: "Mulberry, OOO Demo ©2025",
|
||||||
|
primaryColor: "#002233"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.render('index', {
|
||||||
|
staticImages: Object.keys(staticImages),
|
||||||
|
topics,
|
||||||
|
defaultConfig,
|
||||||
|
connectedDevices: websocket.getConnectedDevices()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'healthy',
|
||||||
|
websocketConnections: websocket.connectedDevices.size
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', gracefulShutdown);
|
||||||
|
process.on('SIGINT', gracefulShutdown);
|
||||||
|
|
||||||
|
function gracefulShutdown() {
|
||||||
|
console.log('Shutting down gracefully...');
|
||||||
|
websocket.cleanup();
|
||||||
|
server.close(() => {
|
||||||
|
console.log('Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/sendToDevice', (req, res) => {
|
||||||
|
const { config, topic } = req.body;
|
||||||
|
console.log(`Sending config to device ${topic}`);
|
||||||
|
const success = websocket.sendToDevice(topic, config);
|
||||||
|
res.status(success ? 200 : 404).send(success ? 'Message sent' : 'Device not connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Payment endpoints
|
||||||
|
// Payment endpoint
|
||||||
|
app.get('/pay', (req, res) => {
|
||||||
|
try {
|
||||||
|
const paymentData = JSON.parse(base64url.decode(req.query.data));
|
||||||
|
|
||||||
|
if (!paymentData.sn || !paymentData.amount) {
|
||||||
|
return res.redirect(`/error?message=${encodeURIComponent('Invalid payment data')}`);
|
||||||
|
}
|
||||||
|
console.log(paymentData);
|
||||||
|
|
||||||
|
// Convert the amount from cents to dollars
|
||||||
|
const amountInDollars = parseFloat(paymentData.amount) / 100;
|
||||||
|
|
||||||
|
// Format the amount to two decimal places
|
||||||
|
const formattedAmount = amountInDollars.toFixed(2);
|
||||||
|
|
||||||
|
const language = paymentData.lang;
|
||||||
|
// Get translations for the selected language
|
||||||
|
const t = translations[language] || translations.en;
|
||||||
|
|
||||||
|
// const event = new Date(paymentData.time);
|
||||||
|
|
||||||
|
// const russianDate = event.toLocaleString("ru-RU", { timeZone: "Etc/GMT-3" })
|
||||||
|
|
||||||
|
|
||||||
|
const event = new Date(paymentData.time);
|
||||||
|
const russianDate = event.toLocaleString("ru-RU", {
|
||||||
|
timeZone: "Europe/Moscow",
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}).replace(',', '');
|
||||||
|
|
||||||
|
// Format data for the receipt
|
||||||
|
const receiptData = {
|
||||||
|
deviceId: paymentData.sn,
|
||||||
|
t:t,
|
||||||
|
appLogo: paymentData.appLogo,
|
||||||
|
appColor: paymentData.appColor,
|
||||||
|
currencyS: paymentData.currencyS,
|
||||||
|
paymentMethod: "QR Code",
|
||||||
|
amount: formattedAmount,
|
||||||
|
timestamp: russianDate
|
||||||
|
,
|
||||||
|
transactionId: `TXN-${Math.random().toString(36).substr(2, 9).toUpperCase()}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render EJS template with data
|
||||||
|
res.render('receipt', {
|
||||||
|
title: 'DatexPay Virtual Bank - Receipt',
|
||||||
|
...receiptData
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.redirect(`/error?message=${encodeURIComponent(error.message)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/pay-command', (req, res) => {
|
||||||
|
console.log("pay command");
|
||||||
|
|
||||||
|
const id = req.query.id;
|
||||||
|
websocket.sendToDevice(id, {
|
||||||
|
type: 'payment',
|
||||||
|
action: 'PRINT_RECEIPT',
|
||||||
|
status : "success" });
|
||||||
|
res.send({ message: 'Receipt print command sent!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/cancel-command', (req, res) => {
|
||||||
|
|
||||||
|
|
||||||
|
const id = req.query.id;
|
||||||
|
console.log("cancel command id :" + id);
|
||||||
|
websocket.sendToDevice(id, { type: 'payment', action: 'REJECT_RECEIPT',
|
||||||
|
status : "failed" });
|
||||||
|
res.send({ message: 'Receipt rejection command sent!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error page
|
||||||
|
app.get('/error', (req, res) => {
|
||||||
|
res.render('error', {
|
||||||
|
title: 'Payment Error',
|
||||||
|
message: req.query.message || 'An unknown error occurred'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// File upload
|
||||||
|
app.post('/upload', upload.single('image'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let base64Data;
|
||||||
|
const isSvg = req.body.isSvg === 'true';
|
||||||
|
|
||||||
|
if (isSvg) {
|
||||||
|
const svgContent = req.file.buffer.toString('utf-8');
|
||||||
|
const pngBuffer = await convertSvgToPng(svgContent, 'LARGE');
|
||||||
|
base64Data = pngBuffer.toString('base64');
|
||||||
|
} else {
|
||||||
|
base64Data = req.file.buffer.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
base64: `data:image/${isSvg ? 'png' : req.file.mimetype.split('/')[1]};base64,${base64Data}`,
|
||||||
|
fileName: req.file.originalname,
|
||||||
|
inputName: req.body.inputName
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
res.status(500).json({ error: 'Image processing failed' });
|
||||||
|
}
|
||||||
|
});
|
71
main copy.js
|
@ -1,71 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const mysql = require('mysql2/promise');
|
|
||||||
const path = require('path');
|
|
||||||
const app = express();
|
|
||||||
const config = require("./config/db.config.js");
|
|
||||||
const port = 8080;
|
|
||||||
|
|
||||||
const dbConfig = {
|
|
||||||
host: config.host,
|
|
||||||
user: config.user,
|
|
||||||
password: config.password,
|
|
||||||
database: config.database,
|
|
||||||
waitForConnections: true,
|
|
||||||
connectionLimit: 10,
|
|
||||||
queueLimit: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use(express.json({ limit: '50mb' }));
|
|
||||||
app.use(express.static(path.join(__dirname, 'public'))); // Serve static files
|
|
||||||
|
|
||||||
async function uploadPriomToMySQL(jsonRows, tableName) {
|
|
||||||
const conn = await mysql.createConnection(dbConfig);
|
|
||||||
|
|
||||||
// This SQL will be used to check for existing records
|
|
||||||
const checkSql = `SELECT COUNT(*) as count FROM ${tableName} WHERE SerialNumber = ? AND filename = ?`;
|
|
||||||
// This SQL will be used to insert new records
|
|
||||||
const insertSql = `INSERT INTO ${tableName} (PriomID, Model, SerialNumber, Problem, filename) VALUES (?, ?, ?, ?, ?)`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(jsonRows.map(async (jsonRow) => {
|
|
||||||
const values = [
|
|
||||||
jsonRow['№ п/п'] || null,
|
|
||||||
jsonRow['Оборудование'] || null,
|
|
||||||
jsonRow['Серийный номер'] || null,
|
|
||||||
jsonRow['Неисправность'] || null,
|
|
||||||
jsonRow['filename'] || null
|
|
||||||
];
|
|
||||||
|
|
||||||
// Check if the combination of SerialNumber and filename exists
|
|
||||||
const [rows] = await conn.execute(checkSql, [values[2], values[4]]);
|
|
||||||
if (rows[0].count === 0) {
|
|
||||||
// Insert only if the combination doesn't exist
|
|
||||||
await conn.execute(insertSql, values);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(err.message);
|
|
||||||
} finally {
|
|
||||||
await conn.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.post('/api/upload', async (req, res) => {
|
|
||||||
// console.log("post upload works")
|
|
||||||
const jsonRows = req.body;
|
|
||||||
|
|
||||||
if (!Array.isArray(jsonRows) ) {
|
|
||||||
return res.status(400).json({ error: 'Invalid request data' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await uploadPriomToMySQL(jsonRows , 'priom');
|
|
||||||
res.status(200).json({ message: 'Data uploaded successfully' });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Internal server error: ' + error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Server running at http://localhost:${port}`);
|
|
||||||
});
|
|
227
main.js
|
@ -1,227 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const base64url = require('base64url');
|
|
||||||
const multer = require('multer');
|
|
||||||
const mqtt = require('mqtt');
|
|
||||||
const { log } = require('console');
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
|
|
||||||
const { convertSvgToPng } = require('./utils/imageConverter');
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
|
||||||
|
|
||||||
// Configure EJS
|
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
|
||||||
|
|
||||||
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
|
||||||
app.use(bodyParser.json({ limit: "50mb" }))
|
|
||||||
|
|
||||||
// Serve static files
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
|
||||||
|
|
||||||
// Static images mapping (matches Android resources)
|
|
||||||
const staticImages = {
|
|
||||||
home: 'am_card_pay_black.png',
|
|
||||||
scan: 'am_barcode_black.png',
|
|
||||||
printer: 'am_receipt_cart_black.png',
|
|
||||||
settings: 'am_settings_black.png',
|
|
||||||
mulberry: 'am_mulberry_logo_wide_color.png',
|
|
||||||
// ... add other images
|
|
||||||
};
|
|
||||||
|
|
||||||
// MQTT Client setup
|
|
||||||
const client = mqtt.connect('mqtt://192.168.1.199', {
|
|
||||||
username: 'ahkad',
|
|
||||||
password: 'Ksenia11241124m'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Topics list
|
|
||||||
const topics = ['pos/update', 'pos/update2', 'pos/update3'];
|
|
||||||
|
|
||||||
// Sample configuration data structure
|
|
||||||
const defaultConfig = {
|
|
||||||
type: "configuration",
|
|
||||||
navItems: [
|
|
||||||
{
|
|
||||||
navPage: "NAV_HOME",
|
|
||||||
navName: "Payment",
|
|
||||||
icon: { type: "resource", value: "home" },
|
|
||||||
position: 0,
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
navPage: "NAV_SCAN",
|
|
||||||
navName: "Scanner",
|
|
||||||
icon: { type: "resource", value: "scan" },
|
|
||||||
position: 1,
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
navPage: "NAV_PRINTER",
|
|
||||||
navName: "Printer",
|
|
||||||
icon: { type: "resource", value: "printer" },
|
|
||||||
position: 2,
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
navPage: "NAV_SETTINGS",
|
|
||||||
navName: "Settings",
|
|
||||||
icon: { type: "resource", value: "settings" },
|
|
||||||
position: 3,
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
mainLogo: {
|
|
||||||
type: "base64",
|
|
||||||
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABlUAAAE "
|
|
||||||
},
|
|
||||||
|
|
||||||
footerLogo: {
|
|
||||||
type: "base64",
|
|
||||||
value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABaQAAAWY"
|
|
||||||
},
|
|
||||||
footerLogoVisibility: "ture",
|
|
||||||
footerGreetingText: "Привет Игорь! ))",
|
|
||||||
footerText: "Mulberry, OOO Demo ©2025",
|
|
||||||
primaryColor: "#002233"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
client.on('connect', () => {
|
|
||||||
console.log('Connected to MQTT broker');
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', (err) => {
|
|
||||||
console.error('MQTT error:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Payment endpoint
|
|
||||||
app.get('/pay', (req, res) => {
|
|
||||||
try {
|
|
||||||
const paymentData = JSON.parse(base64url.decode(req.query.data));
|
|
||||||
|
|
||||||
if (!paymentData.deviceId || !paymentData.paymentMethod || !paymentData.amount) {
|
|
||||||
return res.redirect(`/error?message=${encodeURIComponent('Invalid payment data')}`);
|
|
||||||
}
|
|
||||||
console.log(paymentData);
|
|
||||||
|
|
||||||
// Format data for the receipt
|
|
||||||
const receiptData = {
|
|
||||||
deviceId: paymentData.deviceId,
|
|
||||||
appLogo: paymentData.appLogo,
|
|
||||||
appColor: paymentData.appColor,
|
|
||||||
paymentMethod: paymentData.paymentMethod,
|
|
||||||
amount: paymentData.amount.toFixed(2),
|
|
||||||
timestamp: new Date(paymentData.timestamp).toLocaleString(),
|
|
||||||
transactionId: `TXN-${Math.random().toString(36).substr(2, 9).toUpperCase()}`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render EJS template with data
|
|
||||||
res.render('receipt', {
|
|
||||||
title: 'DatexPay Virtual Bank - Receipt',
|
|
||||||
...receiptData
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
res.redirect(`/error?message=${encodeURIComponent(error.message)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ANSI escape code for grey text
|
|
||||||
const grey = '\x1b[90m';
|
|
||||||
const reset = '\x1b[0m'; // Reset to default color
|
|
||||||
|
|
||||||
// Handle Pay command
|
|
||||||
app.post('/pay-command', (req, res) => {
|
|
||||||
const id = req.query.id; // Get the id from the query parameter
|
|
||||||
const timestamp = new Date().toLocaleString(); // Get the current date and time
|
|
||||||
console.log(`${grey}[${timestamp}] pay command on id: ${id}${reset}`);
|
|
||||||
|
|
||||||
client.publish(id, 'PRINT_RECEIPT');
|
|
||||||
res.send({ message: 'Receipt printed successfully!' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle Cancel command
|
|
||||||
app.post('/cancel-command', (req, res) => {
|
|
||||||
const id = req.query.id; // Get the id from the query parameter
|
|
||||||
const timestamp = new Date().toLocaleString(); // Get the current date and time
|
|
||||||
console.log(`${grey}[${timestamp}] cancel command on id: ${id}${reset}`);
|
|
||||||
|
|
||||||
client.publish(id, 'REJECT_RECEIPT');
|
|
||||||
res.send({ message: 'Receipt rejected!' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error page
|
|
||||||
app.get('/error', (req, res) => {
|
|
||||||
res.render('error', {
|
|
||||||
title: 'Payment Error',
|
|
||||||
message: req.query.message || 'An unknown error occurred'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// app.post('/upload', upload.single('image'), (req, res) => {
|
|
||||||
// const base64 = fs.readFileSync(req.file.path, 'base64');
|
|
||||||
// fs.unlinkSync(req.file.path); // Clean up
|
|
||||||
// res.json({ base64: `data:image/png;base64,${base64}` });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// In your upload route handler
|
|
||||||
app.post('/upload', upload.single('image'), async (req, res) => {
|
|
||||||
try {
|
|
||||||
if (!req.file) {
|
|
||||||
return res.status(400).json({ error: 'No file uploaded' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let base64Data;
|
|
||||||
const isSvg = req.body.isSvg === 'true';
|
|
||||||
|
|
||||||
if (isSvg) {
|
|
||||||
// Convert SVG to PNG
|
|
||||||
const svgContent = req.file.buffer.toString('utf-8');
|
|
||||||
const pngBuffer = await convertSvgToPng(svgContent, 'LARGE');
|
|
||||||
base64Data = pngBuffer.toString('base64');
|
|
||||||
} else {
|
|
||||||
// Use original image for non-SVG
|
|
||||||
base64Data = req.file.buffer.toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
base64: `data:image/${isSvg ? 'png' : req.file.mimetype.split('/')[1]};base64,${base64Data}`,
|
|
||||||
fileName: req.file.originalname,
|
|
||||||
inputName: req.body.inputName
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error);
|
|
||||||
res.status(500).json({ error: 'Image processing failed' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.render('index', {
|
|
||||||
staticImages: Object.keys(staticImages),
|
|
||||||
topics,
|
|
||||||
defaultConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
app.post('/publish', (req, res) => {
|
|
||||||
|
|
||||||
const { config, topic } = req.body;
|
|
||||||
console.log("published config on topic" + topic);
|
|
||||||
client.publish(topic, JSON.stringify(config));
|
|
||||||
res.sendStatus(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
app.listen(80, () => {
|
|
||||||
console.log('Server running on port 80');
|
|
||||||
});
|
|
174
main_test.js
|
@ -1,174 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const app = express();
|
|
||||||
const port = 8080;
|
|
||||||
|
|
||||||
|
|
||||||
const base64url = require('base64url'); // npm install base64url
|
|
||||||
|
|
||||||
app.get('/pay', (req, res) => {
|
|
||||||
try {
|
|
||||||
// Decode and parse the payment data
|
|
||||||
const paymentData = JSON.parse(base64url.decode(req.query.data));
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!paymentData.deviceId || !paymentData.paymentMethod || !paymentData.amount) {
|
|
||||||
return res.status(400).send(invalidRequestResponse());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a beautiful receipt
|
|
||||||
res.send(generateReceiptHTML(
|
|
||||||
paymentData.deviceId,
|
|
||||||
paymentData.paymentMethod,
|
|
||||||
paymentData.amount,
|
|
||||||
paymentData.timestamp
|
|
||||||
));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send(errorResponse(error));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to generate receipt HTML
|
|
||||||
function generateReceiptHTML(deviceId, paymentMethod, amount, timestamp) {
|
|
||||||
const paymentDate = new Date(timestamp).toLocaleString();
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>DatexPay Virtual Bank - Receipt</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
background: #f5f5f5;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.receipt {
|
|
||||||
width: 300px;
|
|
||||||
background: white;
|
|
||||||
padding: 25px;
|
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
|
||||||
border-top: 5px solid #4CAF50;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.header h1 {
|
|
||||||
color: #4CAF50;
|
|
||||||
font-size: 24px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.header p {
|
|
||||||
margin: 5px 0;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.divider {
|
|
||||||
border-top: 1px dashed #ccc;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
.receipt-details {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.receipt-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
.label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="receipt">
|
|
||||||
<div class="header">
|
|
||||||
<h1>DatexPay Virtual Bank</h1>
|
|
||||||
<p>Transaction Receipt</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="receipt-details">
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Device ID:</span>
|
|
||||||
<span>${deviceId}</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Payment Method:</span>
|
|
||||||
<span>${paymentMethod}</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Amount:</span>
|
|
||||||
<span>$${amount.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Date:</span>
|
|
||||||
<span>${paymentDate}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>Thank you for using DatexPay</p>
|
|
||||||
<p>Transaction ID: ${generateTransactionId()}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to generate error response
|
|
||||||
function errorResponse(error) {
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Error - DatexPay</title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
|
||||||
.error-box {
|
|
||||||
border: 1px solid #ff4444;
|
|
||||||
background: #ffeeee;
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="error-box">
|
|
||||||
<h2>Payment Processing Error</h2>
|
|
||||||
<p>${error.message}</p>
|
|
||||||
<p>Please try again or contact support.</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a random transaction ID
|
|
||||||
function generateTransactionId() {
|
|
||||||
return 'TXN-' + Math.random().toString(36).substr(2, 9).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Test server running at http://localhost:${port}/`);
|
|
||||||
});
|
|
|
@ -26,7 +26,8 @@
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"svg2img": "^1.0.0-beta.2",
|
"svg2img": "^1.0.0-beta.2",
|
||||||
"tmp": "^0.2.3"
|
"tmp": "^0.2.3",
|
||||||
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.0"
|
"nodemon": "^3.1.0"
|
||||||
|
|
|
@ -13,7 +13,7 @@ body {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
/* box-shadow: 0 0 10px rgba(0,0,0,0.1); */
|
||||||
border-top: 5px solid #00b8d7;
|
border-top: 5px solid #00b8d7;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
|
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 53 KiB |
|
@ -5,10 +5,12 @@ const imageStore = {
|
||||||
|
|
||||||
// Static image resources
|
// Static image resources
|
||||||
staticImages: {
|
staticImages: {
|
||||||
home: 'am_card_pay_black.png',
|
home: 'payment.png',
|
||||||
scan: 'am_barcode_black.png',
|
scan: 'Scaner.png',
|
||||||
printer: 'am_receipt_cart_black.png',
|
printer: 'Check.png',
|
||||||
settings: 'am_settings_black.png',
|
settings: 'Settings.png',
|
||||||
|
pinpad: 'pinpad.png',
|
||||||
|
cashier: 'cashier.png',
|
||||||
mulberry: 'am_mulberry_logo_wide_color.png',
|
mulberry: 'am_mulberry_logo_wide_color.png',
|
||||||
overtec: 'am_overtec_logo_wide_color.png',
|
overtec: 'am_overtec_logo_wide_color.png',
|
||||||
datexpay: 'am_datexpay_logo_wide_color.png',
|
datexpay: 'am_datexpay_logo_wide_color.png',
|
||||||
|
@ -70,7 +72,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
select.dispatchEvent(new Event('change'));
|
select.dispatchEvent(new Event('change'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Handle image uploads (updated for SVG support)
|
// Handle image uploads (updated for SVG support)
|
||||||
|
@ -130,12 +132,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ERROR] Upload failed:', error);
|
console.error('[ERROR] Upload failed:', error);
|
||||||
alert('Error uploading image. Please try again.');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle image uploads
|
// Handle image uploads
|
||||||
document.querySelectorAll('.icon-upload').forEach(input => {
|
document.querySelectorAll('.icon-upload').forEach(input => {
|
||||||
input.addEventListener('change', async function () {
|
input.addEventListener('change', async function () {
|
||||||
if (!this.files[0]) {
|
if (!this.files[0]) {
|
||||||
|
@ -166,35 +168,35 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Generate a unique key
|
// Generate a unique key
|
||||||
const index = this.closest('.nav-item-config').dataset.index;
|
const index = this.closest('.nav-item-config').dataset.index;
|
||||||
const imageKey = `nav-icon-${index}-${Date.now()}`;
|
const imageKey = `nav-icon-${index}-${Date.now()}`;
|
||||||
|
|
||||||
// Store the image
|
// Store the image
|
||||||
imageStore.storeBase64Nav(imageKey, data.base64);
|
imageStore.storeBase64Nav(imageKey, data.base64);
|
||||||
|
|
||||||
// Update preview dynamically
|
// Update preview dynamically
|
||||||
|
|
||||||
const previewImageElement = document.getElementById(`nav-icon${index}`);
|
const previewImageElement = document.getElementById(`nav-icon${index}`);
|
||||||
if (previewImageElement) {
|
if (previewImageElement) {
|
||||||
previewImageElement.src = data.base64; // Set the src to Base64 data
|
previewImageElement.src = data.base64; // Set the src to Base64 data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the hidden input
|
// Update the hidden input
|
||||||
const hiddenInput = document.querySelector(`input[name="navIconValue${index}"]`);
|
const hiddenInput = document.querySelector(`input[name="navIconValue${index}"]`);
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
hiddenInput.value = imageKey;
|
hiddenInput.value = imageKey;
|
||||||
console.log('Stored nav icon:', {
|
console.log('Stored nav icon:', {
|
||||||
index: index,
|
index: index,
|
||||||
key: imageKey,
|
key: imageKey,
|
||||||
preview: data.base64.substring(0, 20) + '...'
|
preview: data.base64.substring(0, 20) + '...'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update preview
|
// Update preview
|
||||||
const previewImg = document.querySelector(`.nav-item:nth-child(${parseInt(index)+1}) .nav-icon`);
|
const previewImg = document.querySelector(`.nav-item:nth-child(${parseInt(index) + 1}) .nav-icon`);
|
||||||
if (previewImg) {
|
if (previewImg) {
|
||||||
previewImg.src = data.base64;
|
previewImg.src = data.base64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger any additional updates required
|
// Trigger any additional updates required
|
||||||
updatePreview();
|
updatePreview();
|
||||||
|
@ -227,7 +229,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
// .then(response => response.json())
|
// .then(response => response.json())
|
||||||
// .then(data => console.log('Success:', data))
|
// .then(data => console.log('Success:', data))
|
||||||
// .catch(error => console.error('Error:', error));
|
// .catch(error => console.error('Error:', error));
|
||||||
fetch('/publish', {
|
fetch('/sendToDevice', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -237,7 +239,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('[DEBUG] Configuration published successfully');
|
console.log('[DEBUG] Configuration published successfully');
|
||||||
alert('Configuration published successfully!');
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('[ERROR] Publish failed:', err);
|
console.error('[ERROR] Publish failed:', err);
|
||||||
|
@ -305,36 +307,36 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNavItemConfig(formData, prefix) {
|
function getNavItemConfig(formData, prefix) {
|
||||||
const index = prefix.match(/\[(\d+)\]/)[1]; // Extract the index (0, 1, 2, etc.)
|
const index = prefix.match(/\[(\d+)\]/)[1]; // Extract the index (0, 1, 2, etc.)
|
||||||
const iconType = formData.get(`${prefix}[icon][type]`);
|
const iconType = formData.get(`${prefix}[icon][type]`);
|
||||||
let iconValue = formData.get(`${prefix}[icon][value]`);
|
let iconValue = formData.get(`${prefix}[icon][value]`);
|
||||||
|
|
||||||
// For base64 icons, get the value from the hidden input
|
// For base64 icons, get the value from the hidden input
|
||||||
if (iconType === 'base64') {
|
if (iconType === 'base64') {
|
||||||
const storageKey = formData.get(`navIconValue${index}`);
|
const storageKey = formData.get(`navIconValue${index}`);
|
||||||
if (storageKey) {
|
if (storageKey) {
|
||||||
// iconValue = storageKey; // Use the storage key as the value
|
// iconValue = storageKey; // Use the storage key as the value
|
||||||
iconValue = imageStore.getBase64(storageKey)
|
iconValue = imageStore.getBase64(storageKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
console.log("Nav item config:", {
|
console.log("Nav item config:", {
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
index: index,
|
index: index,
|
||||||
iconType: iconType,
|
iconType: iconType,
|
||||||
iconValue: iconValue
|
iconValue: iconValue
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navPage: formData.get(`${prefix}[navPage]`),
|
navPage: formData.get(`${prefix}[navPage]`),
|
||||||
navName: formData.get(`${prefix}[navName]`),
|
navName: formData.get(`${prefix}[navName]`),
|
||||||
icon: {
|
icon: {
|
||||||
type: iconType,
|
type: iconType,
|
||||||
value: iconValue
|
value: iconValue
|
||||||
},
|
},
|
||||||
position: parseInt(formData.get(`${prefix}[position]`)) || 0,
|
position: parseInt(formData.get(`${prefix}[position]`)) || 0,
|
||||||
enabled: formData.get(`${prefix}[enabled]`) === 'on'
|
enabled: formData.get(`${prefix}[enabled]`) === 'on'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function logStoredBase64Images() {
|
function logStoredBase64Images() {
|
||||||
|
@ -396,27 +398,27 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
console.log(`[DEBUG] Set resource icon: ${iconValue}`);
|
console.log(`[DEBUG] Set resource icon: ${iconValue}`);
|
||||||
}
|
}
|
||||||
else if (iconType === 'base64') {
|
else if (iconType === 'base64') {
|
||||||
// Get the index from the prefix
|
// Get the index from the prefix
|
||||||
const index = prefix.match(/\[(\d+)\]/)[1];
|
const index = prefix.match(/\[(\d+)\]/)[1];
|
||||||
const storageKey = formData.get(`navIconValue${index}`);
|
const storageKey = formData.get(`navIconValue${index}`);
|
||||||
|
|
||||||
console.log('Retrieving nav icon:', {
|
console.log('Retrieving nav icon:', {
|
||||||
index: index,
|
index: index,
|
||||||
key: storageKey,
|
key: storageKey,
|
||||||
data: imageStore.getBase64(storageKey)
|
data: imageStore.getBase64(storageKey)
|
||||||
});
|
});
|
||||||
|
|
||||||
const base64Data = imageStore.getBase64(storageKey);
|
const base64Data = imageStore.getBase64(storageKey);
|
||||||
console.log("",);
|
console.log("",);
|
||||||
|
|
||||||
if (base64Data) {
|
if (base64Data) {
|
||||||
iconElement.src = base64Data;
|
iconElement.src = base64Data;
|
||||||
iconElement.style.display = 'block';
|
iconElement.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
console.error('Missing nav icon for:', storageKey);
|
console.error('Missing nav icon for:', storageKey);
|
||||||
iconElement.style.display = 'none';
|
iconElement.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
iconElement.style.display = 'none';
|
iconElement.style.display = 'none';
|
||||||
console.log('[DEBUG] No valid icon to display');
|
console.log('[DEBUG] No valid icon to display');
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// translations.js
|
||||||
|
const translations = {
|
||||||
|
en: {
|
||||||
|
receiptNumber: "Receipt №",
|
||||||
|
paymentType: "Payment Type:",
|
||||||
|
testPayment: "Test Payment:",
|
||||||
|
paymentAddress: "PAYMENT ADDRESS:",
|
||||||
|
issueDate: "ISSUE DATE:",
|
||||||
|
total: "TOTAL:",
|
||||||
|
thankYou: "Thank you for using Mulberry",
|
||||||
|
transactionId: "Transaction ID:",
|
||||||
|
status: "Status:",
|
||||||
|
receiptRejected: "Receipt Rejected",
|
||||||
|
paymentDate: "PAYMENT DATE:",
|
||||||
|
amountToPay: "Amount to Pay:",
|
||||||
|
cancel: "Cancel",
|
||||||
|
pay: "Pay",
|
||||||
|
paymentAddressExample:"Uzbekistan, Tashkent",
|
||||||
|
// Add more translations as needed
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
receiptNumber: "Квитанция №",
|
||||||
|
paymentType: "Тип оплаты:",
|
||||||
|
testPayment: "Тестовый платеж:",
|
||||||
|
paymentAddress: "АДРЕС ПЛАТЕЖА:",
|
||||||
|
issueDate: "ДАТА ВЫПУСКА:",
|
||||||
|
total: "ИТОГО:",
|
||||||
|
thankYou: "Спасибо за использование Mulberry",
|
||||||
|
transactionId: "ID транзакции:",
|
||||||
|
status: "Статус:",
|
||||||
|
receiptRejected: "Квитанция отклонена",
|
||||||
|
paymentDate: "ДАТА ПЛАТЕЖА:",
|
||||||
|
amountToPay: "Сумма к оплате:",
|
||||||
|
cancel: "Отмена",
|
||||||
|
pay: "Оплатить",
|
||||||
|
paymentAddressExample:"Волгоград, пр. им. В. И. Ленина",
|
||||||
|
// Add more translations as needed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = translations;
|
|
@ -4,11 +4,11 @@
|
||||||
<head>
|
<head>
|
||||||
<title>POS Configuration</title>
|
<title>POS Configuration</title>
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
<link rel="stylesheet" href="/css/navbar.css">
|
<link rel="stylesheet" href="/css/navbar.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="navbar-logo">
|
<div class="navbar-logo">
|
||||||
<img src="/images/drawable/am_mulberry_logo_wide_color.png" alt="Mulberry Logo">
|
<img src="/images/drawable/am_mulberry_logo_wide_color.png" alt="Mulberry Logo">
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<!-- Preview Section -->
|
<!-- Preview Section -->
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
|
|
||||||
<div class="pos-terminal">
|
<div class="pos-terminal">
|
||||||
<!-- Main Logo -->
|
<!-- Main Logo -->
|
||||||
<div class="main-logo-preview">
|
<div class="main-logo-preview">
|
||||||
|
@ -41,10 +41,10 @@
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="footer-preview">
|
<div class="footer-preview">
|
||||||
<% if (defaultConfig.footerLogoVisibility) { %>
|
<% if (defaultConfig.footerLogoVisibility) { %>
|
||||||
<img id="footerLogoPreview" src="/images/drawable/am_mulberry_logo_wide_color.png">
|
<img id="footerLogoPreview" src="/images/drawable/am_mulberry_logo_wide_color.png">
|
||||||
<% } %>
|
<% } %>
|
||||||
<div id="footerGreeting">Hello!</div>
|
<div id="footerGreeting">Hello!</div>
|
||||||
<div id="footerText">Mulberry ©2025</div>
|
<div id="footerText">Mulberry ©2025</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,15 +56,15 @@
|
||||||
<label>MQTT Topic</label>
|
<label>MQTT Topic</label>
|
||||||
<select name="topic">
|
<select name="topic">
|
||||||
<% topics.forEach(topic=> { %>
|
<% topics.forEach(topic=> { %>
|
||||||
<option value="<%= topic %>">
|
<option value="<%= topic.deviceID %>">
|
||||||
<%= topic %>
|
<%= topic.deviceName %> (<%= topic.deviceID %>)
|
||||||
</option>
|
</option>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">Save & Publish</button>
|
<button type="submit">Save & Publish</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Main Logo -->
|
<!-- Main Logo -->
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
</option>
|
</option>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<input type="file" name="mainLogoUpload" class="upload-input" style="display:none;">
|
<input type="file" name="mainLogoUpload" class="upload-input" style="display:none;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -104,6 +104,14 @@
|
||||||
>Printer</option>
|
>Printer</option>
|
||||||
<option value="NAV_SETTINGS" <%=item.navPage==='NAV_SETTINGS' ? 'selected' : ''
|
<option value="NAV_SETTINGS" <%=item.navPage==='NAV_SETTINGS' ? 'selected' : ''
|
||||||
%>>Settings</option>
|
%>>Settings</option>
|
||||||
|
|
||||||
|
<option value="NAV_PINPAD" <%=item.navPage==='NAV_PINPAD' ? 'selected' : ''
|
||||||
|
%>>PinPad</option>
|
||||||
|
|
||||||
|
<option value="NAV_CASHIER" <%=item.navPage==='NAV_CASHIER' ? 'selected' : ''
|
||||||
|
%>>Cashier</option>
|
||||||
|
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -113,7 +121,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Icon</label>
|
<label>Icon</label>
|
||||||
<select name="navItems[<%= index %>][icon][type]" onchange="toggleUploadButton(this)">
|
<select name="navItems[<%= index %>][icon][type]"
|
||||||
|
onchange="toggleUploadButton(this)">
|
||||||
<option value="resource" <%=item.icon.type==='resource' ? 'selected' : '' %>
|
<option value="resource" <%=item.icon.type==='resource' ? 'selected' : '' %>
|
||||||
>Static</option>
|
>Static</option>
|
||||||
<option value="base64" <%=item.icon.type==='base64' ? 'selected' : '' %>>Upload
|
<option value="base64" <%=item.icon.type==='base64' ? 'selected' : '' %>>Upload
|
||||||
|
@ -129,8 +138,7 @@
|
||||||
</select>
|
</select>
|
||||||
<input type="file" name="navIconUpload<%= index %>" class="icon-upload"
|
<input type="file" name="navIconUpload<%= index %>" class="icon-upload"
|
||||||
style="display: none;">
|
style="display: none;">
|
||||||
<input type="hidden" name="navIconValue<%= index %>"
|
<input type="hidden" name="navIconValue<%= index %>" navItems
|
||||||
navItems
|
|
||||||
style="display: none;">
|
style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -150,13 +158,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Footer Config -->
|
<!-- Footer Config -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Footer Logo</label>
|
<label>Footer Logo</label>
|
||||||
<select name="footerLogoType">
|
<select name="footerLogoType">
|
||||||
<option value="resource">Static Image</option>
|
<option value="resource">Static Image</option>
|
||||||
<option value="base64">Upload Image</option>
|
<option value="base64">Upload Image</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -171,12 +179,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Footer Logo Visibility</label>
|
<label>Footer Logo Visibility</label>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input type="checkbox" name="footerLogoVisibility" <%= defaultConfig.footerLogoVisibility ? 'checked' : '' %>>
|
<input type="checkbox" name="footerLogoVisibility" <%=defaultConfig.footerLogoVisibility
|
||||||
<span class="slider round"></span>
|
? 'checked' : '' %>>
|
||||||
</label>
|
<span class="slider round"></span>
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Greeting Text</label>
|
<label>Greeting Text</label>
|
||||||
|
@ -194,16 +203,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Primary Color</label>
|
<label>Primary Color</label>
|
||||||
<input type="color" name="primaryColor" value="#002233">
|
<input type="color" name="primaryColor" value="#002233">
|
||||||
<!-- Add this preview button -->
|
<!-- Add this preview button -->
|
||||||
<div class="color-preview" style="margin-top: 10px;">
|
<div class="color-preview" style="margin-top: 10px;">
|
||||||
<button class="preview-button" style="padding: 8px 16px; border-radius: 4px;">
|
<button class="preview-button" style="padding: 8px 16px; border-radius: 4px;">
|
||||||
Submit Button Preview
|
Submit Button Preview
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Log Area -->
|
<!-- Log Area -->
|
||||||
|
|
|
@ -1,182 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title><%= title %></title>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="/css/receipt.css">
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Accepted Receipt -->
|
|
||||||
<div id="acceptedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
|
|
||||||
<div class="header">
|
|
||||||
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
|
|
||||||
<p>чек № 2949</p>
|
|
||||||
<p>https://www.mulberrypos.com</p>
|
|
||||||
<img class="mulberry-logo" src="/images/mulberry-logo-small.png" alt="" srcset="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="receipt-details">
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Тип оплаты:</span>
|
|
||||||
<span><%= paymentMethod %></span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Тестовая оплата:</span>
|
|
||||||
<span><%= amount %> UZS</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">АДРЕС РАСЧЁТОВ:</span>
|
|
||||||
<span style="width: 135px;"> Узбекистан, г. Ташкент</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">ДАТА ВЫДАЧИ:</span>
|
|
||||||
<span><%= timestamp %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="receipt-details">
|
|
||||||
<div style="font-size: large;" class="receipt-row">
|
|
||||||
<span class="label">ИТОГ:</span>
|
|
||||||
<span><%= amount %> UZS </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>Thank you for using Mulberry</p>
|
|
||||||
<p>Transaction ID: <%= transactionId %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rejected Receipt -->
|
|
||||||
<div id="rejectedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
|
|
||||||
<div class="header">
|
|
||||||
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
|
|
||||||
<p>чек № 2949</p>
|
|
||||||
<p>https://www.mulberrypos.com</p>
|
|
||||||
<img class="mulberry-logo" src="/images/mulberry-logo-small.png" alt="" srcset="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="receipt-details">
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">Статус:</span>
|
|
||||||
<span style="color: red;">Чек отклонен</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">ДАТА ПЛАТЕЖА:</span>
|
|
||||||
<span><%= timestamp %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>Transaction ID: <%= transactionId %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="buttons" class="buttons-section" style="border-top: 5px solid <%= appColor %>;">
|
|
||||||
<div class="header">
|
|
||||||
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
|
|
||||||
<p>чек № 2949</p>
|
|
||||||
<p>https://www.mulberrypos.com</p>
|
|
||||||
<img class="mulberry-logo" src="/images/mulberry-logo-small.png" alt="" srcset="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="receipt-details">
|
|
||||||
|
|
||||||
<div style="font-size: large;" class="receipt-row">
|
|
||||||
<span class="label">Сумма к оплате:</span>
|
|
||||||
<span><%= amount %> UZS </span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">ДАТА ПЛАТЕЖА:</span>
|
|
||||||
<span><%= timestamp %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="buttons">
|
|
||||||
<button id="cancelButton">Отмена</button>
|
|
||||||
|
|
||||||
<button id="payButton">Оплата</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>Transaction ID: <%= transactionId %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Buttons -->
|
|
||||||
<div id="buttonsXX" class="buttons">
|
|
||||||
<!-- <button id="payButton">Pay</button>
|
|
||||||
<button id="cancelButton">Cancel</button> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading Circle -->
|
|
||||||
<div id="loading" class="loading"> </div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const acceptedReceipt = document.getElementById('acceptedReceipt');
|
|
||||||
const rejectedReceipt = document.getElementById('rejectedReceipt');
|
|
||||||
const buttons = document.getElementById('buttons');
|
|
||||||
const id = '<%= deviceId %>'; // Pass the id from EJS
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('payButton').addEventListener('click', async () => {
|
|
||||||
const response = await fetch(`/pay-command?id=${id}`, { method: 'POST' });
|
|
||||||
const result = await response.json();
|
|
||||||
loading.style.display = 'block';
|
|
||||||
|
|
||||||
// Simulate payment process for 1 second
|
|
||||||
setTimeout(() => {
|
|
||||||
// Hide loading circle
|
|
||||||
loading.style.display = 'none';
|
|
||||||
|
|
||||||
// Show accepted receipt and hide rejected receipt
|
|
||||||
acceptedReceipt.style.display = 'block';
|
|
||||||
rejectedReceipt.style.display = 'none';
|
|
||||||
buttons.style.display = 'none';
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('cancelButton').addEventListener('click', async () => {
|
|
||||||
const response = await fetch(`/cancel-command?id=${id}`, { method: 'POST' });
|
|
||||||
const result = await response.json();
|
|
||||||
// alert(result.message);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Hide loading circle
|
|
||||||
loading.style.display = 'none';
|
|
||||||
// Show rejected receipt and hide accepted receipt
|
|
||||||
rejectedReceipt.style.display = 'block';
|
|
||||||
acceptedReceipt.style.display = 'none';
|
|
||||||
buttons.style.display = 'none';
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -10,29 +10,30 @@
|
||||||
<!-- Accepted Receipt -->
|
<!-- Accepted Receipt -->
|
||||||
<div id="acceptedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
|
<div id="acceptedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
|
<!-- <img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset=""> -->
|
||||||
<p>Receipt № 2949</p>
|
<img class="datex-pay-logo" src="/images/am_mulberry_logo_wide_color.png" alt="" srcset="">
|
||||||
|
<p><%= t.receiptNumber %> 2949</p>
|
||||||
<!-- <p>https://www.mulberrypos.com</p> -->
|
<!-- <p>https://www.mulberrypos.com</p> -->
|
||||||
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset="">
|
<!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="receipt-details">
|
<div class="receipt-details">
|
||||||
<div class="receipt-row">
|
<div class="receipt-row">
|
||||||
<span class="label">Payment Type:</span>
|
<span class="label"><%= t.paymentType %></span>
|
||||||
<span><%= paymentMethod %></span>
|
<span><%= paymentMethod %></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="receipt-row">
|
<div class="receipt-row">
|
||||||
<span class="label">Test Payment:</span>
|
<span class="label"><%= t.testPayment %></span>
|
||||||
<span><%= amount %> $</span>
|
<span><%= amount %> <%= currencyS %></span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="receipt-row">
|
||||||
|
<span class="label"><%= t.paymentAddress %></span>
|
||||||
|
<span style="width: 135px;"><%= t.paymentAddressExample %></span>
|
||||||
|
</div> -->
|
||||||
<div class="receipt-row">
|
<div class="receipt-row">
|
||||||
<span class="label">PAYMENT ADDRESS:</span>
|
<span class="label"><%= t.issueDate %></span>
|
||||||
<span style="width: 135px;">Uzbekistan, Tashkent</span>
|
|
||||||
</div>
|
|
||||||
<div class="receipt-row">
|
|
||||||
<span class="label">ISSUE DATE:</span>
|
|
||||||
<span><%= timestamp %></span>
|
<span><%= timestamp %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,37 +42,37 @@
|
||||||
|
|
||||||
<div class="receipt-details">
|
<div class="receipt-details">
|
||||||
<div style="font-size: large;" class="receipt-row">
|
<div style="font-size: large;" class="receipt-row">
|
||||||
<span class="label">TOTAL:</span>
|
<span class="label"><%= t.total %></span>
|
||||||
<span><%= amount %> $</span>
|
<span><%= amount %> <%= currencyS %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>Thank you for using Mulberry</p>
|
<p><%= t.thankYou %></p>
|
||||||
<p>Transaction ID: <%= transactionId %></p>
|
<p><%= t.transactionId %> <%= transactionId %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rejected Receipt -->
|
<!-- Rejected Receipt -->
|
||||||
<div id="rejectedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
|
<div id="rejectedReceipt" class="receipt" style="border-top: 5px solid <%= appColor %>;">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
|
<img class="datex-pay-logo" src="/images/am_mulberry_logo_wide_color.png" alt="" srcset="">
|
||||||
<p>Receipt № 2949</p>
|
<p><%= t.receiptNumber %> 2949</p>
|
||||||
<!-- <p>https://www.mulberrypos.com</p> -->
|
<!-- <p>https://www.mulberrypos.com</p> -->
|
||||||
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset="">
|
<!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="receipt-details">
|
<div class="receipt-details">
|
||||||
<div class="receipt-row">
|
<div class="receipt-row">
|
||||||
<span class="label">Status:</span>
|
<span class="label"><%= t.status %></span>
|
||||||
<span style="color: red;">Receipt Rejected</span>
|
<span style="color: red;"><%= t.receiptRejected %></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="receipt-row">
|
<div class="receipt-row">
|
||||||
<span class="label">PAYMENT DATE:</span>
|
<span class="label"><%= t.paymentDate %></span>
|
||||||
<span><%= timestamp %></span>
|
<span><%= timestamp %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,29 +80,29 @@
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>Transaction ID: <%= transactionId %></p>
|
<p><%= t.transactionId %> <%= transactionId %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Buttons Section -->
|
<!-- Buttons Section -->
|
||||||
<div id="buttons" class="buttons-section" style="border-top: 5px solid <%= appColor %>;">
|
<div id="buttons" class="buttons-section" style="border-top: 5px solid <%= appColor %>;">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img class="datex-pay-logo" src="/images/dynamic/<%= appLogo %>.png" alt="" srcset="">
|
<img class="datex-pay-logo" src="/images/am_mulberry_logo_wide_color.png" alt="" srcset="">
|
||||||
<p>Receipt № 2949</p>
|
<p><%= t.receiptNumber %> 2949</p>
|
||||||
<!-- <p>https://www.mulberrypos.com</p> -->
|
<!-- <p>https://www.mulberrypos.com</p> -->
|
||||||
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset="">
|
<!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="receipt-details">
|
<div class="receipt-details">
|
||||||
<div style="font-size: large;" class="receipt-row">
|
<div style="font-size: large;" class="receipt-row">
|
||||||
<span class="label">Amount to Pay:</span>
|
<span class="label"><%= t.amountToPay %></span>
|
||||||
<span><%= amount %> $</span>
|
<span><%= amount %> <%= currencyS %></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="receipt-row">
|
<div class="receipt-row">
|
||||||
<span class="label">PAYMENT DATE:</span>
|
<span class="label"><%= t.paymentDate %></span>
|
||||||
<span><%= timestamp %></span>
|
<span><%= timestamp %></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,14 +110,14 @@
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button id="cancelButton">Cancel</button>
|
<button id="cancelButton"><%= t.cancel %></button>
|
||||||
<button id="payButton">Pay</button>
|
<button id="payButton"><%= t.pay %></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>Transaction ID: <%= transactionId %></p>
|
<p><%= t.transactionId %> <%= transactionId %></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -128,6 +129,11 @@
|
||||||
const rejectedReceipt = document.getElementById('rejectedReceipt');
|
const rejectedReceipt = document.getElementById('rejectedReceipt');
|
||||||
const buttons = document.getElementById('buttons');
|
const buttons = document.getElementById('buttons');
|
||||||
const id = '<%= deviceId %>'; // Pass the id from EJS
|
const id = '<%= deviceId %>'; // Pass the id from EJS
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
console.log("Data in receipt.ejs :");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('payButton').addEventListener('click', async () => {
|
document.getElementById('payButton').addEventListener('click', async () => {
|
||||||
const response = await fetch(`/pay-command?id=${id}`, { method: 'POST' });
|
const response = await fetch(`/pay-command?id=${id}`, { method: 'POST' });
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
class WebSocketManager {
|
||||||
|
constructor() {
|
||||||
|
this.connectedDevices = new Map(); // deviceId -> WebSocket
|
||||||
|
this.wss = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(server) {
|
||||||
|
this.wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
|
this.wss.on('connection', (ws) => {
|
||||||
|
console.log('New client connected');
|
||||||
|
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
|
||||||
|
// Handle device identification
|
||||||
|
if (data.type === 'identification') {
|
||||||
|
this.connectedDevices.set(data.deviceId, ws);
|
||||||
|
console.log(`Device ${data.deviceId} (${data.model}) connected`);
|
||||||
|
|
||||||
|
// Send configuration immediately after identification
|
||||||
|
if (data.requestConfig) {
|
||||||
|
this.sendConfiguration(data.deviceId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'redirect') {
|
||||||
|
console.log(`message ${data.amount}
|
||||||
|
${data.currencySymbol}
|
||||||
|
${data.id} `);
|
||||||
|
|
||||||
|
const ws = this.connectedDevices.get(data.id);
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
|
||||||
|
message = JSON.stringify(data);
|
||||||
|
|
||||||
|
ws.send(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'message') {
|
||||||
|
console.log(`message ${data.content} `);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other message types...
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing message:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
// Clean up disconnected devices
|
||||||
|
for (let [id, connection] of this.connectedDevices.entries()) {
|
||||||
|
if (connection === ws) {
|
||||||
|
this.connectedDevices.delete(id);
|
||||||
|
console.log(`Device ${id} disconnected`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToDevice(deviceId, message) {
|
||||||
|
const ws = this.connectedDevices.get(deviceId);
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
if (typeof message !== 'string') {
|
||||||
|
message = JSON.stringify(message);
|
||||||
|
}
|
||||||
|
ws.send(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(`Device ${deviceId} not connected`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendConfiguration(deviceId) {
|
||||||
|
// In a real app, you would fetch device-specific config from database
|
||||||
|
const config = {
|
||||||
|
type: "configuration",
|
||||||
|
deviceId: deviceId,
|
||||||
|
navItems: [{
|
||||||
|
navPage: "NAV_HOME",
|
||||||
|
navName: "PaymentZ",
|
||||||
|
icon: { type: "resource", value: "home" },
|
||||||
|
position: 0,
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
navPage: "NAV_SCAN",
|
||||||
|
navName: "ScannerZ",
|
||||||
|
icon: { type: "resource", value: "scan" },
|
||||||
|
position: 1,
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
this.sendToDevice(deviceId, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(message) {
|
||||||
|
this.connectedDevices.forEach((ws, deviceId) => {
|
||||||
|
this.sendToDevice(deviceId, message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectedDevices() {
|
||||||
|
return Array.from(this.connectedDevices.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new WebSocketManager();
|