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:
“VeLiTi”
2025-12-24 14:07:28 +01:00
parent 0f968aac14
commit 543f0b3cac
21 changed files with 1400 additions and 238 deletions

View File

@@ -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']]);
}
}
}

View File

@@ -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