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",
|
||||
"sharp": "^0.34.3",
|
||||
"svg2img": "^1.0.0-beta.2",
|
||||
"tmp": "^0.2.3"
|
||||
"tmp": "^0.2.3",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
|
|
|
@ -13,7 +13,7 @@ body {
|
|||
width: 300px;
|
||||
background: white;
|
||||
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;
|
||||
}
|
||||
.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
|
||||
staticImages: {
|
||||
home: 'am_card_pay_black.png',
|
||||
scan: 'am_barcode_black.png',
|
||||
printer: 'am_receipt_cart_black.png',
|
||||
settings: 'am_settings_black.png',
|
||||
home: 'payment.png',
|
||||
scan: 'Scaner.png',
|
||||
printer: 'Check.png',
|
||||
settings: 'Settings.png',
|
||||
pinpad: 'pinpad.png',
|
||||
cashier: 'cashier.png',
|
||||
mulberry: 'am_mulberry_logo_wide_color.png',
|
||||
overtec: 'am_overtec_logo_wide_color.png',
|
||||
datexpay: 'am_datexpay_logo_wide_color.png',
|
||||
|
@ -70,7 +72,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
select.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Handle image uploads (updated for SVG support)
|
||||
|
@ -130,12 +132,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
} catch (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 => {
|
||||
input.addEventListener('change', async function () {
|
||||
if (!this.files[0]) {
|
||||
|
@ -166,35 +168,35 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
const data = await response.json();
|
||||
|
||||
// Generate a unique key
|
||||
const index = this.closest('.nav-item-config').dataset.index;
|
||||
const imageKey = `nav-icon-${index}-${Date.now()}`;
|
||||
|
||||
// Store the image
|
||||
imageStore.storeBase64Nav(imageKey, data.base64);
|
||||
|
||||
const index = this.closest('.nav-item-config').dataset.index;
|
||||
const imageKey = `nav-icon-${index}-${Date.now()}`;
|
||||
|
||||
// Store the image
|
||||
imageStore.storeBase64Nav(imageKey, data.base64);
|
||||
|
||||
// Update preview dynamically
|
||||
|
||||
|
||||
const previewImageElement = document.getElementById(`nav-icon${index}`);
|
||||
if (previewImageElement) {
|
||||
previewImageElement.src = data.base64; // Set the src to Base64 data
|
||||
}
|
||||
|
||||
// Update the hidden input
|
||||
const hiddenInput = document.querySelector(`input[name="navIconValue${index}"]`);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = imageKey;
|
||||
console.log('Stored nav icon:', {
|
||||
index: index,
|
||||
key: imageKey,
|
||||
preview: data.base64.substring(0, 20) + '...'
|
||||
});
|
||||
}
|
||||
// Update the hidden input
|
||||
const hiddenInput = document.querySelector(`input[name="navIconValue${index}"]`);
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = imageKey;
|
||||
console.log('Stored nav icon:', {
|
||||
index: index,
|
||||
key: imageKey,
|
||||
preview: data.base64.substring(0, 20) + '...'
|
||||
});
|
||||
}
|
||||
|
||||
// Update preview
|
||||
const previewImg = document.querySelector(`.nav-item:nth-child(${parseInt(index)+1}) .nav-icon`);
|
||||
if (previewImg) {
|
||||
previewImg.src = data.base64;
|
||||
}
|
||||
// Update preview
|
||||
const previewImg = document.querySelector(`.nav-item:nth-child(${parseInt(index) + 1}) .nav-icon`);
|
||||
if (previewImg) {
|
||||
previewImg.src = data.base64;
|
||||
}
|
||||
|
||||
// Trigger any additional updates required
|
||||
updatePreview();
|
||||
|
@ -227,7 +229,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
// .then(response => response.json())
|
||||
// .then(data => console.log('Success:', data))
|
||||
// .catch(error => console.error('Error:', error));
|
||||
fetch('/publish', {
|
||||
fetch('/sendToDevice', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
@ -237,7 +239,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
})
|
||||
.then(() => {
|
||||
console.log('[DEBUG] Configuration published successfully');
|
||||
alert('Configuration published successfully!');
|
||||
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[ERROR] Publish failed:', err);
|
||||
|
@ -305,36 +307,36 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
}
|
||||
|
||||
function getNavItemConfig(formData, prefix) {
|
||||
const index = prefix.match(/\[(\d+)\]/)[1]; // Extract the index (0, 1, 2, etc.)
|
||||
const iconType = formData.get(`${prefix}[icon][type]`);
|
||||
let iconValue = formData.get(`${prefix}[icon][value]`);
|
||||
const index = prefix.match(/\[(\d+)\]/)[1]; // Extract the index (0, 1, 2, etc.)
|
||||
const iconType = formData.get(`${prefix}[icon][type]`);
|
||||
let iconValue = formData.get(`${prefix}[icon][value]`);
|
||||
|
||||
// For base64 icons, get the value from the hidden input
|
||||
if (iconType === 'base64') {
|
||||
const storageKey = formData.get(`navIconValue${index}`);
|
||||
if (storageKey) {
|
||||
// iconValue = storageKey; // Use the storage key as the value
|
||||
iconValue = imageStore.getBase64(storageKey)
|
||||
if (iconType === 'base64') {
|
||||
const storageKey = formData.get(`navIconValue${index}`);
|
||||
if (storageKey) {
|
||||
// iconValue = storageKey; // Use the storage key as the value
|
||||
iconValue = imageStore.getBase64(storageKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Nav item config:", {
|
||||
prefix: prefix,
|
||||
index: index,
|
||||
iconType: iconType,
|
||||
iconValue: iconValue
|
||||
});
|
||||
prefix: prefix,
|
||||
index: index,
|
||||
iconType: iconType,
|
||||
iconValue: iconValue
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
navPage: formData.get(`${prefix}[navPage]`),
|
||||
navName: formData.get(`${prefix}[navName]`),
|
||||
icon: {
|
||||
type: iconType,
|
||||
value: iconValue
|
||||
},
|
||||
position: parseInt(formData.get(`${prefix}[position]`)) || 0,
|
||||
enabled: formData.get(`${prefix}[enabled]`) === 'on'
|
||||
};
|
||||
return {
|
||||
navPage: formData.get(`${prefix}[navPage]`),
|
||||
navName: formData.get(`${prefix}[navName]`),
|
||||
icon: {
|
||||
type: iconType,
|
||||
value: iconValue
|
||||
},
|
||||
position: parseInt(formData.get(`${prefix}[position]`)) || 0,
|
||||
enabled: formData.get(`${prefix}[enabled]`) === 'on'
|
||||
};
|
||||
}
|
||||
|
||||
function logStoredBase64Images() {
|
||||
|
@ -396,27 +398,27 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
console.log(`[DEBUG] Set resource icon: ${iconValue}`);
|
||||
}
|
||||
else if (iconType === 'base64') {
|
||||
// Get the index from the prefix
|
||||
const index = prefix.match(/\[(\d+)\]/)[1];
|
||||
const storageKey = formData.get(`navIconValue${index}`);
|
||||
|
||||
console.log('Retrieving nav icon:', {
|
||||
index: index,
|
||||
key: storageKey,
|
||||
data: imageStore.getBase64(storageKey)
|
||||
});
|
||||
|
||||
const base64Data = imageStore.getBase64(storageKey);
|
||||
console.log("",);
|
||||
|
||||
if (base64Data) {
|
||||
iconElement.src = base64Data;
|
||||
iconElement.style.display = 'block';
|
||||
} else {
|
||||
console.error('Missing nav icon for:', storageKey);
|
||||
iconElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// Get the index from the prefix
|
||||
const index = prefix.match(/\[(\d+)\]/)[1];
|
||||
const storageKey = formData.get(`navIconValue${index}`);
|
||||
|
||||
console.log('Retrieving nav icon:', {
|
||||
index: index,
|
||||
key: storageKey,
|
||||
data: imageStore.getBase64(storageKey)
|
||||
});
|
||||
|
||||
const base64Data = imageStore.getBase64(storageKey);
|
||||
console.log("",);
|
||||
|
||||
if (base64Data) {
|
||||
iconElement.src = base64Data;
|
||||
iconElement.style.display = 'block';
|
||||
} else {
|
||||
console.error('Missing nav icon for:', storageKey);
|
||||
iconElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
else {
|
||||
iconElement.style.display = 'none';
|
||||
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>
|
||||
<title>POS Configuration</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/navbar.css">
|
||||
<link rel="stylesheet" href="/css/navbar.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<div class="navbar">
|
||||
<div class="navbar-logo">
|
||||
<img src="/images/drawable/am_mulberry_logo_wide_color.png" alt="Mulberry Logo">
|
||||
</div>
|
||||
|
@ -21,7 +21,7 @@
|
|||
|
||||
<!-- Preview Section -->
|
||||
<div class="preview">
|
||||
|
||||
|
||||
<div class="pos-terminal">
|
||||
<!-- Main Logo -->
|
||||
<div class="main-logo-preview">
|
||||
|
@ -41,10 +41,10 @@
|
|||
<!-- Footer -->
|
||||
<div class="footer-preview">
|
||||
<% if (defaultConfig.footerLogoVisibility) { %>
|
||||
<img id="footerLogoPreview" src="/images/drawable/am_mulberry_logo_wide_color.png">
|
||||
<% } %>
|
||||
<div id="footerGreeting">Hello!</div>
|
||||
<div id="footerText">Mulberry ©2025</div>
|
||||
<img id="footerLogoPreview" src="/images/drawable/am_mulberry_logo_wide_color.png">
|
||||
<% } %>
|
||||
<div id="footerGreeting">Hello!</div>
|
||||
<div id="footerText">Mulberry ©2025</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -56,15 +56,15 @@
|
|||
<label>MQTT Topic</label>
|
||||
<select name="topic">
|
||||
<% topics.forEach(topic=> { %>
|
||||
<option value="<%= topic %>">
|
||||
<%= topic %>
|
||||
<option value="<%= topic.deviceID %>">
|
||||
<%= topic.deviceName %> (<%= topic.deviceID %>)
|
||||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
<button type="submit">Save & Publish</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Main Logo -->
|
||||
|
@ -81,7 +81,7 @@
|
|||
</option>
|
||||
<% }); %>
|
||||
</select>
|
||||
|
||||
|
||||
<input type="file" name="mainLogoUpload" class="upload-input" style="display:none;">
|
||||
</div>
|
||||
|
||||
|
@ -104,6 +104,14 @@
|
|||
>Printer</option>
|
||||
<option value="NAV_SETTINGS" <%=item.navPage==='NAV_SETTINGS' ? 'selected' : ''
|
||||
%>>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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -113,7 +121,8 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<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' : '' %>
|
||||
>Static</option>
|
||||
<option value="base64" <%=item.icon.type==='base64' ? 'selected' : '' %>>Upload
|
||||
|
@ -129,8 +138,7 @@
|
|||
</select>
|
||||
<input type="file" name="navIconUpload<%= index %>" class="icon-upload"
|
||||
style="display: none;">
|
||||
<input type="hidden" name="navIconValue<%= index %>"
|
||||
navItems
|
||||
<input type="hidden" name="navIconValue<%= index %>" navItems
|
||||
style="display: none;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -150,13 +158,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Footer Config -->
|
||||
<div class="form-group">
|
||||
<label>Footer Logo</label>
|
||||
<select name="footerLogoType">
|
||||
<select name="footerLogoType">
|
||||
<option value="resource">Static Image</option>
|
||||
<option value="base64">Upload Image</option>
|
||||
</select>
|
||||
|
@ -171,12 +179,13 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Footer Logo Visibility</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="footerLogoVisibility" <%= defaultConfig.footerLogoVisibility ? 'checked' : '' %>>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
<label>Footer Logo Visibility</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" name="footerLogoVisibility" <%=defaultConfig.footerLogoVisibility
|
||||
? 'checked' : '' %>>
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Greeting Text</label>
|
||||
|
@ -194,16 +203,16 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Primary Color</label>
|
||||
<input type="color" name="primaryColor" value="#002233">
|
||||
<!-- Add this preview button -->
|
||||
<div class="color-preview" style="margin-top: 10px;">
|
||||
<button class="preview-button" style="padding: 8px 16px; border-radius: 4px;">
|
||||
Submit Button Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Primary Color</label>
|
||||
<input type="color" name="primaryColor" value="#002233">
|
||||
<!-- Add this preview button -->
|
||||
<div class="color-preview" style="margin-top: 10px;">
|
||||
<button class="preview-button" style="padding: 8px 16px; border-radius: 4px;">
|
||||
Submit Button Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- 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 -->
|
||||
<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>Receipt № 2949</p>
|
||||
<!-- <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><%= t.receiptNumber %> 2949</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 class="divider"></div>
|
||||
|
||||
|
||||
<div class="receipt-details">
|
||||
<div class="receipt-row">
|
||||
<span class="label">Payment Type:</span>
|
||||
<span class="label"><%= t.paymentType %></span>
|
||||
<span><%= paymentMethod %></span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="label">Test Payment:</span>
|
||||
<span><%= amount %> $</span>
|
||||
<span class="label"><%= t.testPayment %></span>
|
||||
<span><%= amount %> <%= currencyS %></span>
|
||||
</div>
|
||||
<!-- <div class="receipt-row">
|
||||
<span class="label"><%= t.paymentAddress %></span>
|
||||
<span style="width: 135px;"><%= t.paymentAddressExample %></span>
|
||||
</div> -->
|
||||
<div class="receipt-row">
|
||||
<span class="label">PAYMENT ADDRESS:</span>
|
||||
<span style="width: 135px;">Uzbekistan, Tashkent</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="label">ISSUE DATE:</span>
|
||||
<span class="label"><%= t.issueDate %></span>
|
||||
<span><%= timestamp %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,37 +42,37 @@
|
|||
|
||||
<div class="receipt-details">
|
||||
<div style="font-size: large;" class="receipt-row">
|
||||
<span class="label">TOTAL:</span>
|
||||
<span><%= amount %> $</span>
|
||||
<span class="label"><%= t.total %></span>
|
||||
<span><%= amount %> <%= currencyS %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Thank you for using Mulberry</p>
|
||||
<p>Transaction ID: <%= transactionId %></p>
|
||||
<p><%= t.thankYou %></p>
|
||||
<p><%= t.transactionId %> <%= 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>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> -->
|
||||
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset="">
|
||||
<!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="receipt-details">
|
||||
<div class="receipt-row">
|
||||
<span class="label">Status:</span>
|
||||
<span style="color: red;">Receipt Rejected</span>
|
||||
<span class="label"><%= t.status %></span>
|
||||
<span style="color: red;"><%= t.receiptRejected %></span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="label">PAYMENT DATE:</span>
|
||||
<span class="label"><%= t.paymentDate %></span>
|
||||
<span><%= timestamp %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -79,29 +80,29 @@
|
|||
<div class="divider"></div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Transaction ID: <%= transactionId %></p>
|
||||
<p><%= t.transactionId %> <%= transactionId %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons Section -->
|
||||
<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>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> -->
|
||||
<img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset="">
|
||||
<!-- <img class="mulberry-logo" src="/images/dynamic/overtec.png" alt="" srcset=""> -->
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="receipt-details">
|
||||
<div style="font-size: large;" class="receipt-row">
|
||||
<span class="label">Amount to Pay:</span>
|
||||
<span><%= amount %> $</span>
|
||||
<span class="label"><%= t.amountToPay %></span>
|
||||
<span><%= amount %> <%= currencyS %></span>
|
||||
</div>
|
||||
|
||||
<div class="receipt-row">
|
||||
<span class="label">PAYMENT DATE:</span>
|
||||
<span class="label"><%= t.paymentDate %></span>
|
||||
<span><%= timestamp %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -109,14 +110,14 @@
|
|||
<div class="divider"></div>
|
||||
|
||||
<div class="buttons">
|
||||
<button id="cancelButton">Cancel</button>
|
||||
<button id="payButton">Pay</button>
|
||||
<button id="cancelButton"><%= t.cancel %></button>
|
||||
<button id="payButton"><%= t.pay %></button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Transaction ID: <%= transactionId %></p>
|
||||
<p><%= t.transactionId %> <%= transactionId %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -128,6 +129,11 @@
|
|||
const rejectedReceipt = document.getElementById('rejectedReceipt');
|
||||
const buttons = document.getElementById('buttons');
|
||||
const id = '<%= deviceId %>'; // Pass the id from EJS
|
||||
console.log(id);
|
||||
|
||||
console.log("Data in receipt.ejs :");
|
||||
|
||||
|
||||
|
||||
document.getElementById('payButton').addEventListener('click', async () => {
|
||||
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();
|