feat: Add software licenses management page and update payment handling
- Introduced a new licenses management page with functionality to create, update, and view software licenses. - Updated payment return handling in softwaretool.php to check payment status from the database and display appropriate modals for success, pending, and failure states. - Enhanced webhook_mollie.php to log webhook calls, handle payment status updates directly in the database, and generate invoices based on payment status. - Improved CSS styles for better alignment of buttons and modal components. - Added JavaScript for modal interactions and bulk license creation functionality.
This commit is contained in:
@@ -1418,7 +1418,8 @@ function getWhereclauselvl2($table_name,$permission,$partner,$method){
|
||||
"software" => "p.accounthierarchy",
|
||||
"transactions" => "tx.accounthierarchy",
|
||||
"dealers" => "d.accounthierarchy",
|
||||
"categories" => "c.accounthierarchy"
|
||||
"categories" => "c.accounthierarchy",
|
||||
"products_software_licenses" => "l.accounthierarchy"
|
||||
];
|
||||
|
||||
$table = ($table_name != '') ? $table[$table_name] : 'accounthierarchy';
|
||||
@@ -5154,23 +5155,7 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) {
|
||||
$stmt->execute($bind_params);
|
||||
|
||||
//------------------------------------------
|
||||
// STEP 3: Set sw_version_latest = 0 for equipment NOT matching latest version
|
||||
//------------------------------------------
|
||||
$sql = 'UPDATE equipment e
|
||||
JOIN products_software_assignment psa ON e.productrowid = psa.product_id AND psa.status = 1
|
||||
JOIN products_software_versions psv ON psa.software_version_id = psv.rowID
|
||||
SET e.sw_version_latest = 0
|
||||
WHERE psv.latest = 1
|
||||
AND psv.status = 1
|
||||
AND lower(e.sw_version) <> lower(psv.version)
|
||||
AND (psv.hw_version = e.hw_version OR psv.hw_version IS NULL OR psv.hw_version = "")
|
||||
AND e.sw_version_latest = 1' . $sn_clause;
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($bind_params);
|
||||
|
||||
//------------------------------------------
|
||||
// STEP 4: Set sw_version_latest = 1 for equipment matching latest version
|
||||
// STEP 3: Set sw_version_latest = 1 for equipment matching latest version
|
||||
//------------------------------------------
|
||||
$sql = 'UPDATE equipment e
|
||||
JOIN products_software_assignment psa ON e.productrowid = psa.product_id AND psa.status = 1
|
||||
@@ -5179,7 +5164,7 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) {
|
||||
WHERE psv.latest = 1
|
||||
AND psv.status = 1
|
||||
AND lower(e.sw_version) = lower(psv.version)
|
||||
AND (psv.hw_version = e.hw_version OR psv.hw_version IS NULL OR psv.hw_version = "")
|
||||
AND (lower(psv.hw_version) = lower(e.hw_version) OR lower(psv.hw_version) IS NULL OR lower(psv.hw_version) = "")
|
||||
AND e.sw_version_latest = 0' . $sn_clause;
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
@@ -5542,4 +5527,49 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
|
||||
</html>';
|
||||
|
||||
return [$html, $customer_email, $order_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update latest flags for software versions
|
||||
* Max 2 latest flags per hw_version: 1 with price (has upgrade path with price) and 1 without
|
||||
*
|
||||
* @param PDO $pdo - Database connection
|
||||
* @param int $version_id - The version ID being set as latest
|
||||
* @param string $hw_version - Hardware version
|
||||
*/
|
||||
function updateSoftwareLatestFlags($pdo, $version_id, $hw_version) {
|
||||
//Check if current version has a priced upgrade path
|
||||
$sql = 'SELECT COUNT(*) as has_price
|
||||
FROM products_software_upgrade_paths
|
||||
WHERE to_version_id = ? AND is_active = 1 AND price > 0';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$version_id]);
|
||||
$current_has_price = $stmt->fetch(PDO::FETCH_ASSOC)['has_price'] > 0;
|
||||
|
||||
//Remove latest flag only from versions in the same category (priced or free)
|
||||
//Get all versions with same hw_version and check their pricing
|
||||
$sql = 'SELECT psv.rowID,
|
||||
CASE
|
||||
WHEN EXISTS(
|
||||
SELECT 1 FROM products_software_upgrade_paths pup
|
||||
WHERE pup.to_version_id = psv.rowID
|
||||
AND pup.is_active = 1
|
||||
AND pup.price > 0
|
||||
) THEN 1
|
||||
ELSE 0
|
||||
END as has_price
|
||||
FROM products_software_versions psv
|
||||
WHERE psv.hw_version = ? AND psv.rowID != ?';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$hw_version, $version_id]);
|
||||
$versions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
//Update only versions in the same price category
|
||||
foreach ($versions as $version) {
|
||||
if ($version['has_price'] == ($current_has_price ? 1 : 0)) {
|
||||
$sql = 'UPDATE products_software_versions SET latest = 0 WHERE rowID = ?';
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$version['rowID']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,132 +459,143 @@ function displaySoftwareOptions(options) {
|
||||
const isFree = price === 0;
|
||||
const isCurrent = option.is_current === true || option.is_current === 1;
|
||||
|
||||
// Create card
|
||||
// Create card with gradient background
|
||||
const card = document.createElement("div");
|
||||
card.style.cssText = `
|
||||
background: ${isCurrent ? '#f5f5f5' : 'white'};
|
||||
border: 2px solid ${isCurrent ? '#bbb' : (isFree ? '#e0e0e0' : '#e0e0e0')};
|
||||
background: ${isCurrent ? 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)' : 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)'};
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
transition: 0.3s;
|
||||
padding: 25px 20px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
transform: translateY(0px);
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;
|
||||
opacity: ${isCurrent ? '0.6' : '1'};
|
||||
box-shadow: ${isCurrent ? '0 4px 12px rgba(0,0,0,0.08)' : '0 8px 20px rgba(0,0,0,0.12)'};
|
||||
opacity: ${isCurrent ? '0.7' : '1'};
|
||||
pointer-events: ${isCurrent ? 'none' : 'auto'};
|
||||
min-height: 320px;
|
||||
`;
|
||||
|
||||
if (!isCurrent) {
|
||||
card.onmouseenter = () => {
|
||||
card.style.transform = 'translateY(-5px)';
|
||||
card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.15)';
|
||||
card.style.transform = 'translateY(-8px) scale(1.02)';
|
||||
card.style.boxShadow = '0 12px 28px rgba(0,0,0,0.2)';
|
||||
card.style.borderColor = isFree ? '#038f5a' : '#FF4500';
|
||||
};
|
||||
card.onmouseleave = () => {
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
|
||||
card.style.transform = 'translateY(0) scale(1)';
|
||||
card.style.boxShadow = '0 8px 20px rgba(0,0,0,0.12)';
|
||||
card.style.borderColor = isFree ? '#04AA6D' : '#FF6B35';
|
||||
};
|
||||
}
|
||||
|
||||
// Badge for current/free/paid
|
||||
// Badge for current/free/paid - VISIBLE
|
||||
const badge = document.createElement("div");
|
||||
badge.style.cssText = `
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: ${isCurrent ? '#6c757d' : '#04AA6D'};
|
||||
top: -10px;
|
||||
right: 20px;
|
||||
background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')};
|
||||
color: white;
|
||||
padding: 5px 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
display:none;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
`;
|
||||
|
||||
if (isCurrent) {
|
||||
badge.textContent = "CURRENT VERSION";
|
||||
badge.textContent = "INSTALLED";
|
||||
} else if (isFree) {
|
||||
badge.textContent = "Included";
|
||||
badge.textContent = "FREE";
|
||||
} else {
|
||||
badge.textContent = "PREMIUM";
|
||||
}
|
||||
|
||||
if (isCurrent || isFree) {
|
||||
card.appendChild(badge);
|
||||
}
|
||||
card.appendChild(badge);
|
||||
|
||||
// Name
|
||||
// Name with icon
|
||||
const name = document.createElement("h4");
|
||||
name.style.cssText = `
|
||||
margin: 0 0 10px 0;
|
||||
margin: 0 0 12px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
name.textContent = option.name || "Software Update";
|
||||
name.innerHTML = `<i class="fa-solid fa-microchip" style="color: ${isFree ? '#04AA6D' : '#FF6B35'}; margin-right: 8px;"></i>${option.name || "Software Update"}`;
|
||||
card.appendChild(name);
|
||||
|
||||
// Version
|
||||
// Version with enhanced styling
|
||||
const version = document.createElement("div");
|
||||
version.style.cssText = `
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`;
|
||||
version.innerHTML = `<i class="fa-solid fa-code-branch"></i> Version: <strong>${option.version || "N/A"}</strong>`;
|
||||
version.innerHTML = `<i class="fa-solid fa-code-branch" style="color: #999;"></i> <span style="font-weight: 500;">Version:</span> <strong>${option.version || "N/A"}</strong>`;
|
||||
card.appendChild(version);
|
||||
|
||||
// Description
|
||||
const desc = document.createElement("p");
|
||||
desc.style.cssText = `
|
||||
// Description with preserved newlines
|
||||
const descContainer = document.createElement("div");
|
||||
descContainer.style.cssText = `
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
margin: 0 0 20px 0;
|
||||
flex-grow: 1;
|
||||
white-space: pre-line;
|
||||
`;
|
||||
desc.textContent = option.description || "No description available";
|
||||
card.appendChild(desc);
|
||||
descContainer.textContent = option.description || "No description available";
|
||||
card.appendChild(descContainer);
|
||||
|
||||
// Price section
|
||||
const priceSection = document.createElement("div");
|
||||
priceSection.style.cssText = `
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 15px;
|
||||
border-top: 2px solid ${isFree ? '#04AA6D20' : '#FF6B3520'};
|
||||
padding-top: 20px;
|
||||
margin-top: auto;
|
||||
`;
|
||||
|
||||
const priceText = document.createElement("div");
|
||||
priceText.style.cssText = `
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#333')};
|
||||
font-size: ${isCurrent ? '18px' : '28px'};
|
||||
font-weight: ${isCurrent ? '600' : '800'};
|
||||
color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#FF6B35')};
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.5px;
|
||||
`;
|
||||
|
||||
if (isCurrent) {
|
||||
priceText.textContent = "INSTALLED";
|
||||
priceText.innerHTML = '<i class="fa-solid fa-check-circle"></i> INSTALLED';
|
||||
} else {
|
||||
priceText.textContent = isFree ? "Included" : `${option.currency || "€"} ${price.toFixed(2)}`;
|
||||
priceText.innerHTML = isFree
|
||||
? 'Free'
|
||||
: `${option.currency || "€"} ${price.toFixed(2)}`;
|
||||
}
|
||||
|
||||
priceSection.appendChild(priceText);
|
||||
|
||||
// Action button
|
||||
// Action button with gradient for paid
|
||||
const actionBtn = document.createElement("button");
|
||||
actionBtn.className = "btn";
|
||||
actionBtn.style.cssText = `
|
||||
width: 100%;
|
||||
background: ${isCurrent ? '#6c757d' : '#04AA6D'};
|
||||
background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')};
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: ${isCurrent ? 'not-allowed' : 'pointer'};
|
||||
transition: background 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
opacity: ${isCurrent ? '0.5' : '1'};
|
||||
box-shadow: ${isCurrent ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'};
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
if (isCurrent) {
|
||||
@@ -593,13 +604,29 @@ function displaySoftwareOptions(options) {
|
||||
} else if (isFree) {
|
||||
actionBtn.innerHTML = '<i class="fa-solid fa-download"></i>';
|
||||
actionBtn.onclick = () => selectUpgrade(option);
|
||||
actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a';
|
||||
actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D';
|
||||
actionBtn.onmouseenter = () => {
|
||||
actionBtn.style.background = 'linear-gradient(135deg, #038f5a 0%, #026b43 100%)';
|
||||
actionBtn.style.transform = 'translateY(-2px)';
|
||||
actionBtn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.2)';
|
||||
};
|
||||
actionBtn.onmouseleave = () => {
|
||||
actionBtn.style.background = 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)';
|
||||
actionBtn.style.transform = 'translateY(0)';
|
||||
actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||
};
|
||||
} else {
|
||||
actionBtn.innerHTML = '<i class="fa-solid fa-shopping-cart"></i>';
|
||||
actionBtn.onclick = () => selectUpgrade(option);
|
||||
actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a';
|
||||
actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D';
|
||||
actionBtn.onmouseenter = () => {
|
||||
actionBtn.style.background = 'linear-gradient(135deg, #FF4500 0%, #CC3700 100%)';
|
||||
actionBtn.style.transform = 'translateY(-2px)';
|
||||
actionBtn.style.boxShadow = '0 6px 16px rgba(255,107,53,0.4)';
|
||||
};
|
||||
actionBtn.onmouseleave = () => {
|
||||
actionBtn.style.background = 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)';
|
||||
actionBtn.style.transform = 'translateY(0)';
|
||||
actionBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
|
||||
};
|
||||
}
|
||||
|
||||
priceSection.appendChild(actionBtn);
|
||||
@@ -980,10 +1007,19 @@ async function processPayment(paymentData, option, modal) {
|
||||
user_data: paymentData // name, email, address only
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
console.log("=== DEBUG: Payment Request ===");
|
||||
console.log("Serial Number:", deviceSerialNumber);
|
||||
console.log("Version ID:", option.version_id);
|
||||
console.log("User Data:", paymentData);
|
||||
console.log("Request payload:", paymentRequest);
|
||||
}
|
||||
|
||||
await logCommunication(`Payment initiated for version ${option.version_id}`, 'sent');
|
||||
|
||||
// Call payment API to create Mollie payment
|
||||
const response = await fetch(link + "/v2/post/payment", {
|
||||
const response = await fetch(link + "/v2/payment", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -994,13 +1030,27 @@ async function processPayment(paymentData, option, modal) {
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
console.error("DEBUG: Payment API error:", errorData);
|
||||
}
|
||||
throw new Error(errorData.error || "Failed to create payment");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
console.log("=== DEBUG: Payment Response ===");
|
||||
console.log("Result:", result);
|
||||
console.log("Checkout URL:", result.checkout_url);
|
||||
console.log("Payment ID:", result.payment_id);
|
||||
}
|
||||
|
||||
if (result.checkout_url) {
|
||||
await logCommunication(`Redirecting to payment provider`, 'sent');
|
||||
await logCommunication(`Redirecting to Mollie payment: ${result.payment_id}`, 'sent');
|
||||
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
console.log("DEBUG: Redirecting to Mollie checkout...");
|
||||
}
|
||||
|
||||
// Close modal before redirect
|
||||
document.body.removeChild(modal);
|
||||
@@ -1012,6 +1062,9 @@ async function processPayment(paymentData, option, modal) {
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
console.error("DEBUG: Payment processing error:", error);
|
||||
}
|
||||
await logCommunication(`Payment error: ${error.message}`, 'error');
|
||||
progressBar("0", "Payment failed: " + error.message, "#ff6666");
|
||||
alert("Payment failed: " + error.message);
|
||||
@@ -1028,7 +1081,7 @@ async function downloadAndInstallSoftware(option, customerData = null) {
|
||||
if (paymentId) {
|
||||
try {
|
||||
// Verify serial number matches payment
|
||||
const response = await fetch(link + `/v2/get/payment?payment_id=${paymentId}`, {
|
||||
const response = await fetch(link + `/v2/payment?payment_id=${paymentId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
|
||||
|
||||
Reference in New Issue
Block a user