feat: Implement invoice generation and emailing functionality

- Added invoice generation logic using DomPDF.
- Integrated invoice data retrieval from the API.
- Implemented language determination for invoices based on customer data.
- Added options to email invoices to customers and admin.
- Included HTML output option for direct viewing in the browser.
- Ensured proper redirection and error handling throughout the process.
This commit is contained in:
“VeLiTi”
2026-01-07 14:36:48 +01:00
parent 543f0b3cac
commit 08263c7933
46 changed files with 4982 additions and 151 deletions

View File

@@ -1672,7 +1672,7 @@ function overviewIndicators($warranty, $service, $sw_version, $sw_version_latest
$indicator .= '<span class="dot" style="background-color: #13b368;">F</span>';
} else {
if ($sw_version == ''){
$indicator .= '<span class="dot" style="background-color: #81848a">F</span>';
$indicator .= '<span class="dot" style="background-color: #13b368;">F</span>';
} else {
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">F</span>';
}
@@ -1777,7 +1777,7 @@ function availableFirmware($sw_version,$sw_version_latest){
break;
default:
$message ='<span class="status">Unknown</span>';
$message ='<span class="status"></span>';
break;
}
@@ -5339,10 +5339,12 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
$customer_country = $customer['address_country'] ?? '';
// Extract transaction data
$payment_amount = $invoice_data['payment_amount'] ?? 0;
$tax_amount = $invoice_data['tax_amount'] ?? 0;
$shipping_amount = $invoice_data['shipping_amount'] ?? 0;
$discount_amount = $invoice_data['discount_amount'] ?? 0;
$pricing = $invoice_data['pricing'] ?? [];
$payment_amount = $pricing['payment_amount'] ?? $invoice_data['payment_amount'] ?? 0;
$tax_amount = $pricing['tax_total'] ?? $invoice_data['tax_amount'] ?? 0;
$shipping_amount = $pricing['shipping_total'] ?? $invoice_data['shipping_amount'] ?? 0;
$discount_amount = $pricing['discount_total'] ?? $invoice_data['discount_amount'] ?? 0;
$subtotal_amount = $pricing['subtotal'] ?? 0;
$currency = 'EUR'; // Default currency
$invoice_date = $invoice_data['invoice_created'] ?? date('Y-m-d H:i:s');
@@ -5373,6 +5375,39 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
'serial_number' => $serial_number,
'license_key' => $license_key
];
} elseif (isset($invoice_data['products']) && is_array($invoice_data['products'])) {
// New format with products array
$pdo = dbConnect($dbname);
foreach ($invoice_data['products'] as $product) {
$product_code = $product['productcode'] ?? null;
$product_name = $product['product_name'] ?? null;
$product_options = $product['options'] ?? [];
$product_serial = $product_options['serial_number'] ?? null;
// Handle case where productcode and product_name are empty but serial_number exists
if ((empty($product_code) || $product_code === null) &&
(empty($product_name) || $product_name === null) &&
!empty($product_serial)) {
$product_code = 'License';
$product_name = 'software license for ' . $product_serial;
}
// Get license key from database
$sql = 'SELECT license_key FROM products_software_licenses WHERE transaction_id = ? LIMIT 1';
$stmt = $pdo->prepare($sql);
$stmt->execute([$order_id]);
$license_result = $stmt->fetch(PDO::FETCH_ASSOC);
$license_key = $license_result['license_key'] ?? 'Pending';
$items[] = [
'name' => $product_name ?? 'Software Upgrade',
'quantity' => $product['quantity'] ?? 1,
'price' => $product['price'] ?? 0,
'serial_number' => $product_serial ?? 'N/A',
'license_key' => $license_key
];
}
}
// Load language translations
@@ -5402,127 +5437,319 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
$lbl_license_key = $translations['license_key'] ?? 'License Key';
$lbl_license_expiry = $translations['license_expiry'] ?? 'License Expiry';
// Subtotal calculation - use from pricing data or calculate from items
if ($subtotal_amount > 0) {
$subtotal = $subtotal_amount;
} else {
// Calculate from items if not provided
$subtotal = 0;
foreach ($items as $item) {
$subtotal += $item['price'] * $item['quantity'];
}
}
// Build HTML invoice
$html = '<!DOCTYPE html>
<html>
<html lang="' . strtolower($language) . '">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>' . htmlspecialchars($lbl_invoice) . ' - Total Safety Solutions</title>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; color: #333; }
.invoice-header { margin-bottom: 30px; }
.invoice-title { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
.invoice-info { margin-bottom: 20px; }
.customer-info { margin-bottom: 20px; background: #f5f5f5; padding: 15px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th { background: #4CAF50; color: white; padding: 10px; text-align: left; }
td { padding: 10px; border-bottom: 1px solid #ddd; }
.text-right { text-align: right; }
.total-row { font-weight: bold; background: #f9f9f9; }
.license-info { background: #e3f2fd; padding: 15px; margin-top: 20px; border-left: 4px solid #2196F3; }
.footer { margin-top: 40px; text-align: center; font-size: 10px; color: #666; }
@page {margin: 220px 50px; }
body {
font-family: "DejaVu Sans", Arial, sans-serif;
color: #000;
font-size: 11px;
line-height: 1.4;
margin: 0;
padding: 0;
}
#header {
position: fixed;
left: -52px;
top: -220px;
right: -50px;
height: 200px;
text-align: center;
border-radius: 5px;
}
#header img {
width: 100%;
}
#footer {
position: fixed;
left: -50px;
bottom: -250px;
right: -50px;
height: 150px;
border-radius: 5px;
}
#footer img {
width: 100%;
}
.invoice-title {
font-size: 18px;
font-weight: bold;
color: #2c5f5d;
margin-bottom: 20px;
}
.company-header {
display: table;
width: 100%;
margin-bottom: 25px;
}
.company-info, .contact-details {
display: table-cell;
vertical-align: top;
width: 50%;
}
.company-info h3 {
font-weight: bold;
margin: 0 0 5px 0;
color: #2c5f5d;
font-size: 12px;
}
.company-info p, .contact-details p {
margin: 0;
line-height: 1.2;
}
.contact-details {
text-align: right;
}
.contact-details h3 {
font-weight: bold;
margin: 0 0 5px 0;
color: #2c5f5d;
font-size: 12px;
}
.invoice-details {
display: table;
width: 100%;
margin-bottom: 25px;
}
.invoice-left, .invoice-right {
display: table-cell;
vertical-align: top;
width: 50%;
}
.invoice-right {
text-align: left;
}
.detail-row {
margin-bottom: 2px;
display: table;
width: 100%;
}
.detail-label {
display: table-cell;
font-weight: normal;
width: 140px;
padding-right: 10px;
}
.detail-value {
display: table-cell;
}
.detail-value {
display: table-cell;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
border-bottom: 1px solid #999;
}
.items-table th {
padding: 8px 6px;
font-weight: bold;
text-align: left;
font-size: 10px;
}
.items-table td {
padding: 6px;
border-bottom: 1px solid #999;
font-size: 10px;
}
.items-table .qty-col {
text-align: center;
width: 70px;
}
.items-table .price-col, .items-table .total-col {
text-align: right;
width: 80px;
}
.items-table th.qty-col {
text-align: center;
}
.items-table th.price-col, .items-table th.total-col {
text-align: right;
}
.totals-section {
float: right;
width: 250px;
margin-top: 20px;
clear: both;
}
.total-row {
display: table;
width: 100%;
margin-bottom: 3px;
}
.total-label {
display: table-cell;
text-align: left;
width: 150px;
}
.total-amount {
display: table-cell;
text-align: right;
font-weight: normal;
}
.final-total {
border-top: 1px solid #000;
padding-top: 5px;
margin-top: 8px;
}
.final-total .total-amount {
font-weight: bold;
}
</style>
</head>
<body>
<div class="invoice-header">
<div class="invoice-title">' . htmlspecialchars($lbl_invoice) . '</div>
<div class="invoice-info">
<strong>' . htmlspecialchars($lbl_invoice_number) . ':</strong> ' . htmlspecialchars($order_id) . '<br>
<strong>' . htmlspecialchars($lbl_invoice_date) . ':</strong> ' . htmlspecialchars(date('Y-m-d', strtotime($invoice_date))) . '
<div id="header">
<img src="https://'.$portalURL.'/assets/images/TSS_invoice_header.png" alt="Invoice header">
</div>
<div id="footer">
<img src="https://'.$portalURL.'/assets/images/TSS_invoice_footer.png" alt="Invoice footer">
</div>
<div class="invoice-title">' . htmlspecialchars($lbl_invoice) . '</div>
<div class="company-header">
<div class="company-info">
<h3>Total Safety Solutions B.V.</h3>
<p>Laarakkerweg 8</p>
<p>5061 JR OISTERWIJK</p>
<p>Nederland</p>
</div>
<div class="contact-details">
</div>
</div>
<div class="customer-info">
<strong>' . htmlspecialchars($lbl_customer) . ':</strong><br>
' . htmlspecialchars($customer_name) . '<br>';
if ($customer_address) {
$html .= htmlspecialchars($customer_address) . '<br>';
}
if ($customer_city || $customer_zip) {
$html .= htmlspecialchars($customer_zip . ' ' . $customer_city) . '<br>';
}
if ($customer_state) {
$html .= htmlspecialchars($customer_state) . '<br>';
}
if ($customer_country) {
$html .= htmlspecialchars($customer_country) . '<br>';
}
$html .= htmlspecialchars($customer_email) . '
<div class="invoice-details">
<div class="invoice-left">
<div class="detail-row">
<div class="detail-label">Invoice Date</div>
<div class="detail-value">: ' . htmlspecialchars(date('d-m-Y', strtotime($invoice_date))) . '</div>
</div>
<div class="detail-row">
<div class="detail-label">Invoice Number</div>
<div class="detail-value">: ' . htmlspecialchars($order_id) . '</div>
</div>
</div>
<div class="invoice-right">
<div class="detail-row">
<div class="detail-label">Reference</div>
<div class="detail-value">: Online order</div>
</div>
<div class="detail-row">
<div class="detail-label">Order number</div>
<div class="detail-value">: ' . htmlspecialchars($order_id) . '</div>
</div>
</div>
</div>
<table>
<table class="items-table">
<thead>
<tr>
<th>' . htmlspecialchars($lbl_product) . '</th>
<th class="text-right">' . htmlspecialchars($lbl_quantity) . '</th>
<th class="text-right">' . htmlspecialchars($lbl_price) . '</th>
<th>Item code</th>
<th>Description</th>
<th class="qty-col">Quantity</th>
<th class="price-col">Price</th>
<th class="total-col">Total</th>
</tr>
</thead>
<tbody>';
foreach ($items as $item) {
$line_total = $item['price'] * $item['quantity'];
$html .= '<tr>
<td>' . htmlspecialchars($item['name']) . '</td>
<td class="text-right">' . htmlspecialchars($item['quantity']) . '</td>
<td class="text-right">' . number_format($item['price'], 2) . ' ' . htmlspecialchars($currency) . '</td>
</tr>';
<td>SOFTWARE</td>
<td>' . htmlspecialchars($item['name']);
if ($item['serial_number'] !== 'N/A') {
$html .= '<br><small>Serial: ' . htmlspecialchars($item['serial_number']) . '</small>';
}
if ($item['license_key'] !== 'Pending') {
$html .= '<br><small>License: ' . htmlspecialchars($item['license_key']) . '</small>';
}
$html .= '</td>
<td class="qty-col">' . htmlspecialchars($item['quantity']) . ' </td>
<td class="price-col">€ ' . number_format($item['price'], 2) . '</td>
<td class="total-col">€ ' . number_format($line_total, 2) . '</td>
</tr>';
}
// Subtotal
$subtotal = $payment_amount - $tax_amount - $shipping_amount + $discount_amount;
$html .= '<tr>
<td colspan="2" class="text-right"><strong>' . htmlspecialchars($lbl_subtotal) . ':</strong></td>
<td class="text-right">' . number_format($subtotal, 2) . ' ' . htmlspecialchars($currency) . '</td>
</tr>';
// Tax
if ($tax_amount > 0) {
$html .= '<tr>
<td colspan="2" class="text-right"><strong>' . htmlspecialchars($lbl_tax) . ':</strong></td>
<td class="text-right">' . number_format($tax_amount, 2) . ' ' . htmlspecialchars($currency) . '</td>
</tr>';
}
// Shipping
if ($shipping_amount > 0) {
$html .= '<tr>
<td colspan="2" class="text-right"><strong>' . htmlspecialchars($lbl_shipping) . ':</strong></td>
<td class="text-right">' . number_format($shipping_amount, 2) . ' ' . htmlspecialchars($currency) . '</td>
</tr>';
}
// Discount
if ($discount_amount > 0) {
$html .= '<tr>
<td colspan="2" class="text-right"><strong>' . htmlspecialchars($lbl_discount) . ':</strong></td>
<td class="text-right">-' . number_format($discount_amount, 2) . ' ' . htmlspecialchars($currency) . '</td>
</tr>';
}
// Total
$html .= '<tr class="total-row">
<td colspan="2" class="text-right"><strong>' . htmlspecialchars($lbl_total) . ':</strong></td>
<td class="text-right"><strong>' . number_format($payment_amount, 2) . ' ' . htmlspecialchars($currency) . '</strong></td>
</tr>';
$html .= '</tbody>
</table>';
</table>
// License information
if ($license_key && $serial_number) {
$html .= '<div class="license-info">
<strong>Software License Information:</strong><br>
<strong>' . htmlspecialchars($lbl_device_serial) . ':</strong> ' . htmlspecialchars($serial_number) . '<br>
<strong>' . htmlspecialchars($lbl_license_key) . ':</strong> ' . htmlspecialchars($license_key) . '<br>
<strong>' . htmlspecialchars($lbl_license_expiry) . ':</strong> 2099-12-31
</div>';
<div class="totals-section">
<div class="total-row">
<div class="total-label">' . htmlspecialchars($lbl_subtotal) . '</div>
<div class="total-amount">€ ' . number_format($subtotal, 2) . '</div>
</div>';
if ($tax_amount > 0) {
$html .= '<div class="total-row">
<div class="total-label">' . htmlspecialchars($lbl_tax) . '</div>
<div class="total-amount">€ ' . number_format($tax_amount, 2) . '</div>
</div>';
} else {
$html .= '<div class="total-row">
<div class="total-label">VAT</div>
<div class="total-amount">included</div>
</div>';
}
$html .= '<div class="footer">
Thank you for your purchase!<br>
This invoice was generated automatically.
$html .= '<div class="total-row final-total">
<div class="total-label">' . htmlspecialchars($lbl_total) . '</div>
<div class="total-amount">€ ' . number_format($payment_amount, 2) . '</div>
</div>
</div>
</body>
</html>';

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -119,7 +119,10 @@ async function connectDevice() {
// Log connection failure details
await logCommunication(`Serial connection failed: ${error.message || 'Unknown error'}`, 'disconnected');
if (openPort = 1){
// Check for specific "No port selected" error and show user-friendly message
if (error.message && error.message.includes('No port selected by the user')) {
progressBar("100", "No device selected, please try again", "#ff6666");
} else if (openPort = 1){
closePort();
console.log("Closing port");
alert("System is still trying to close the serial port. If this message continues to come up please refresh this page.");

View File

@@ -161,7 +161,13 @@ async function connectDeviceForSoftware() {
} catch (error) {
await logCommunication(`Connection error: ${error.message}`, 'error');
progressBar("0", "Error: " + error.message, "#ff6666");
// Check for specific "No port selected" error and show user-friendly message
if (error.message && error.message.includes('No port selected by the user')) {
progressBar("100", "No device selected, please try again", "#ff6666");
} else {
progressBar("100", "Error: " + error.message, "#ff6666");
}
}
}
@@ -1193,14 +1199,50 @@ async function downloadAndInstallSoftware(option, customerData = null) {
console.log("Click the 'Install Software' button to test if upload.js can handle the file");
alert("DEBUG MODE: Download complete!\n\nBlob size: " + blob.size + " bytes\n\nClick the 'Install Software' button to test upload.js");
} else {
// PRODUCTION MODE: Show upload button and automatically trigger
// PRODUCTION MODE: Hide button and show installation in progress
document.getElementById("uploadSection").style.display = "block";
const uploadBtn = document.getElementById("uploadSoftware");
uploadBtn.style.display = "block";
uploadBtn.disabled = false;
uploadBtn.style.display = "none";
// Hide device version information during installation
const softwareOptions = document.getElementById("softwareOptions");
if (softwareOptions) {
softwareOptions.style.display = "none";
}
// Create installation status indicator
const installStatus = document.createElement("div");
installStatus.id = "installationStatus";
installStatus.style.cssText = `
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 8px;
margin: 10px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
`;
installStatus.innerHTML = `
<i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: #04AA6D; margin-bottom: 10px;"></i>
<p style="margin: 0; font-size: 18px; font-weight: 600; color: #333;">Installing Software...</p>
<p style="margin: 5px 0 0 0; color: #666; font-size: 14px;">Please keep your device connected and do not close this page</p>
`;
// Insert status before the hidden upload section
document.getElementById("uploadSection").parentNode.insertBefore(installStatus, document.getElementById("uploadSection"));
progressBar("60", "Ready to install, starting upload...", "#04AA6D");
uploadBtn.click();
progressBar("60", "Starting automatic installation...", "#04AA6D");
// Enable the upload button and automatically click it
setTimeout(() => {
uploadBtn.disabled = false;
// Start monitoring for completion
if (typeof startUploadMonitoring === 'function') {
startUploadMonitoring();
}
uploadBtn.click();
}, 1000);
}
} catch (error) {