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

3
.gitignore vendored
View File

@@ -12,3 +12,6 @@ settings/config.php
variable_scan.php
settings/soveliti/soveliti_config.php
settings/soveliti/soveliti_settings.php
assets/database/dev_schema.sql
assets/database/migration.sql
assets/database/prod_schema.sql

View File

@@ -49,6 +49,9 @@ if(isset($get_content) && $get_content!=''){
elseif ($v[0] == 'equipmentid') {
//build up search
$clause .= ' AND e.rowID = :'.$v[0];
//UPDATE VERSION STATUS
$sw_version_latest_update = 1;
}
elseif ($v[0] == 'servicedate') {
//build up service coverage
@@ -69,6 +72,7 @@ if(isset($get_content) && $get_content!=''){
elseif ($v[0] == 'h_equipmentid') {
//build up search
$clause .= ' AND h.equipmentid = :'.$v[0];
}
elseif ($v[0] == 'status') {
//Update status based on status

View File

@@ -1,6 +1,8 @@
<?php
defined($security_key) or exit;
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
//------------------------------------------
// Products Software Licenses
//------------------------------------------
@@ -12,7 +14,7 @@ $pdo = dbConnect($dbname);
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
list($whereclause,$condition) = getWhereclauselvl2("software_licenses",$permission,$partner,'get');
list($whereclause,$condition) = getWhereclauselvl2("products_software_licenses",$permission,$partner,'get');
//NEW ARRAY
$criterias = [];
@@ -53,11 +55,19 @@ if(isset($criterias['totals']) && $criterias['totals'] ==''){
}
elseif (isset($criterias['list']) && $criterias['list'] =='') {
//SQL for list
$sql = 'SELECT l.*, u.username, v.name as version_name FROM products_software_licenses l LEFT JOIN users u ON l.user_id = u.id LEFT JOIN products_software_versions v ON l.version_id = v.rowID '.$whereclause.' ORDER BY l.created DESC';
$sql = 'SELECT l.*, v.name as version_name, v.version, e.serialnumber as assigned_serial
FROM products_software_licenses l
LEFT JOIN products_software_versions v ON l.version_id = v.rowID
LEFT JOIN equipment e ON l.license_key = e.sw_version_license
'.$whereclause.' ORDER BY l.created DESC';
}
else {
//SQL for paged
$sql = 'SELECT l.*, u.username, v.name as version_name FROM products_software_licenses l LEFT JOIN users u ON l.user_id = u.id LEFT JOIN products_software_versions v ON l.version_id = v.rowID '.$whereclause.' ORDER BY l.created DESC LIMIT :page,:num_licenses';
$sql = 'SELECT l.*, v.name as version_name, v.version, e.serialnumber as assigned_serial
FROM products_software_licenses l
LEFT JOIN products_software_versions v ON l.version_id = v.rowID
LEFT JOIN equipment e ON l.license_key = e.sw_version_license
'.$whereclause.' ORDER BY l.created DESC LIMIT :page,:num_licenses';
}
$stmt = $pdo->prepare($sql);

View File

@@ -245,16 +245,16 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
//Check if there's a valid license for this upgrade
if ($final_price > 0 && $sw_version_license) {
//Check if the license is valid
$sql = 'SELECT status, start_at, expires_at
$sql = 'SELECT status, starts_at, expires_at
FROM products_software_licenses
WHERE license_key = ? AND equipment_id = ?';
WHERE license_key = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$sw_version_license, $equipment_rowid]);
$stmt->execute([$sw_version_license]);
$license = $stmt->fetch(PDO::FETCH_ASSOC);
if ($license && $license['status'] == 1) {
$now = date('Y-m-d H:i:s');
$start_at = $license['start_at'];
$start_at = $license['starts_at'];
$expires_at = $license['expires_at'];
//Check if license is within valid date range

View File

@@ -281,16 +281,16 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$license_applied = false;
if ($final_price > 0 && $sw_version_license) {
//Check if the license is valid
$sql = 'SELECT status, start_at, expires_at
$sql = 'SELECT status, starts_at, expires_at
FROM products_software_licenses
WHERE license_key = ? AND equipment_id = ?';
WHERE license_key = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$sw_version_license, $equipment_rowid]);
$stmt->execute([$sw_version_license]);
$license = $stmt->fetch(PDO::FETCH_ASSOC);
if ($license && $license['status'] == 1) {
$now = date('Y-m-d H:i:s');
$start_at = $license['start_at'];
$start_at = $license['starts_at'];
$expires_at = $license['expires_at'];
//Check if license is within valid date range

View File

@@ -354,6 +354,42 @@ elseif(isset($post_content['dealer_closeby'])){
echo json_encode(['error' => "Latitude or longitude not provided."]);
}
}
elseif(isset($post_content['action']) && $post_content['action']=='unsubscribe'){
//++++++++++++++++++++++
//Process DEALER UNSUBSCRIBE
//++++++++++++++++++++++
// Check if email is provided
if (isset($post_content['email']) && !empty($post_content['email'])) {
$email = $post_content['email'];
try {
// Update dealer status to 0 (inactive) where email matches
$sql = 'UPDATE dealers SET status = 0 WHERE email = ?';
$stmt = $pdo->prepare($sql);
if ($stmt->execute([$email])) {
// Check if any rows were affected
if ($stmt->rowCount() > 0) {
header('Content-Type: application/json');
echo json_encode(['status' => 'success', 'message' => 'Dealer unsubscribed successfully']);
} else {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'No dealer found with this email']);
}
} else {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Database update failed']);
}
} catch (PDOException $e) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Database error occurred']);
}
} else {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Email not provided']);
}
}
else
{
//++++++++++++++++++++++

View File

@@ -1,11 +1,12 @@
<?php
defined($security_key) or exit;
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
//------------------------------------------
// Payment Creation (for Software Upgrades)
//------------------------------------------
// This endpoint creates a Mollie payment and stores transaction data
// SECURITY: Price is calculated SERVER-SIDE, never trusted from frontend
//Connect to DB
$pdo = dbConnect($dbname);
@@ -13,6 +14,7 @@ $pdo = dbConnect($dbname);
//CONTENT FROM API (POST)
$post_content = json_decode($input, true);
// Validate required inputs
if (empty($post_content['serial_number']) || empty($post_content['version_id'])) {
http_response_code(400);
@@ -33,6 +35,7 @@ $stmt->execute([$serial_number]);
$equipment = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$equipment) {
http_response_code(404);
echo json_encode(['error' => 'Device not found with serial number: ' . $serial_number], JSON_UNESCAPED_UNICODE);
exit;
@@ -46,15 +49,15 @@ $hw_version = $equipment['hw_version'] ?? '';
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 2: Get version data from version_id
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$sql = 'SELECT v.rowID as version_id, v.version, v.name, v.description, v.hw_version, p.productcode
FROM products_software_versions v
JOIN products_software p ON v.product_software_id = p.rowID
WHERE v.rowID = ? AND v.is_active = 1';
$sql = 'SELECT rowID as version_id, version, name, description, hw_version
FROM products_software_versions
WHERE rowID = ? AND status = 1';
$stmt = $pdo->prepare($sql);
$stmt->execute([$version_id]);
$version = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$version) {
http_response_code(404);
echo json_encode(['error' => 'Software version not found or inactive'], JSON_UNESCAPED_UNICODE);
exit;
@@ -93,6 +96,7 @@ if (!$has_upgrade_paths) {
$final_currency = $upgrade_path['currency'] ?? 'EUR';
} else {
// No upgrade path FROM current version
http_response_code(400);
echo json_encode(['error' => 'No valid upgrade path from current version'], JSON_UNESCAPED_UNICODE);
exit;
@@ -103,20 +107,20 @@ if (!$has_upgrade_paths) {
// STEP 4: Check license validity (lines 280-311 in software_update.php)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($final_price > 0 && $sw_version_license) {
$sql = 'SELECT status, start_at, expires_at
$sql = 'SELECT status, starts_at, expires_at
FROM products_software_licenses
WHERE license_key = ? AND equipment_id = ?';
WHERE license_key = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$sw_version_license, $equipment_id]);
$stmt->execute([$sw_version_license]);
$license = $stmt->fetch(PDO::FETCH_ASSOC);
if ($license && $license['status'] == 1) {
$now = date('Y-m-d H:i:s');
$start_at = $license['start_at'];
$starts_at = $license['starts_at'];
$expires_at = $license['expires_at'];
// Check if license is within valid date range
if ((!$start_at || $start_at <= $now) && (!$expires_at || $expires_at >= $now)) {
if ((!$starts_at || $starts_at <= $now) && (!$expires_at || $expires_at >= $now)) {
$final_price = '0.00';
}
}
@@ -126,68 +130,18 @@ if ($final_price > 0 && $sw_version_license) {
// STEP 5: Verify price > 0 (free upgrades shouldn't reach payment API)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($final_price <= 0) {
http_response_code(400);
echo json_encode(['error' => 'This upgrade is free. No payment required.'], JSON_UNESCAPED_UNICODE);
exit;
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 6: DEBUG MODE - Simulate payment without Mollie
// STEP 6: DEBUG MODE - Log but continue to real Mollie
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if (debug) {
// Generate fake payment ID
$fake_payment_id = 'DEBUG_' . uniqid() . '_' . time();
$checkout_url = 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id=' . $fake_payment_id;
// Store transaction in DB
$sql = 'INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name,
address_street, address_city, address_state, address_zip, address_country, account_id, payment_method, created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$fake_payment_id,
$final_price,
0, // 0 = pending
$user_data['email'] ?? '',
$user_data['first_name'] ?? '',
$user_data['last_name'] ?? '',
$user_data['address_street'] ?? '',
$user_data['address_city'] ?? '',
$user_data['address_state'] ?? '',
$user_data['address_zip'] ?? '',
$user_data['address_country'] ?? '',
$serial_number,
0, // payment method
date('Y-m-d H:i:s')
]);
// Store transaction item with serial_number in item_options
$item_options = json_encode([
'serial_number' => $serial_number,
'equipment_id' => $equipment_id,
'hw_version' => $hw_version
], JSON_UNESCAPED_UNICODE);
$sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created)
VALUES (?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$fake_payment_id,
$version_id,
$final_price,
1,
$item_options,
date('Y-m-d H:i:s')
]);
// Return fake checkout URL
$messages = json_encode([
'checkout_url' => $checkout_url,
'payment_id' => $fake_payment_id,
'debug_mode' => true
], JSON_UNESCAPED_UNICODE);
echo $messages;
exit;
debuglog("DEBUG MODE: Creating real Mollie payment for testing");
debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price");
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -195,66 +149,110 @@ if (debug) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
try {
// Initialize Mollie
require dirname(__FILE__, 3).'/initialize.php';
require dirname(__FILE__, 4).'/initialize.php';
// Format price for Mollie (must be string with 2 decimals)
$formatted_price = number_format((float)$final_price, 2, '.', '');
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 7A: Generate transaction ID BEFORE creating Mollie payment
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Generate unique transaction ID (same as placeorder.php)
$txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5));
// Build webhook URL and redirect URL with actual transaction ID
$protocol = 'https';
$hostname = $_SERVER['SERVER_NAME'];
$path = '/';
$webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php";
$redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}";
if (debug) {
debuglog("DEBUG: Transaction ID: {$txn_id}");
debuglog("DEBUG: redirectUrl being sent to Mollie: " . $redirect_url);
}
// Create payment with Mollie
$payment = $mollie->payments->create([
'amount' => [
'currency' => $final_currency ?: 'EUR',
'value' => $formatted_price
'value' => "{$formatted_price}"
],
'description' => 'Software upgrade to ' . $version['name'] . ' (v' . $version['version'] . ')',
'redirectUrl' => 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id={id}',
'webhookUrl' => 'https://'.$_SERVER['SERVER_NAME'].'/webhook_mollie.php',
'description' => "Software upgrade Order #{$txn_id}",
'redirectUrl' => "{$redirect_url}",
'webhookUrl' => "{$webhook_url}",
'metadata' => [
'order_id' => $payment->id // Store payment ID in metadata
'order_id' => $txn_id,
'serial_number' => $serial_number,
'version_id' => $version_id,
'equipment_id' => $equipment_id
]
]);
$mollie_payment_id = $payment->id;
$checkout_url = $payment->getCheckoutUrl();
if (debug) {
debuglog("DEBUG: Mollie payment created successfully");
debuglog("DEBUG: Payment ID: $mollie_payment_id");
debuglog("DEBUG: Redirect URL sent: $redirect_url");
debuglog("DEBUG: Redirect URL from Mollie object: " . $payment->redirectUrl);
debuglog("DEBUG: Full payment object: " . json_encode($payment));
debuglog("DEBUG: Checkout URL: $checkout_url");
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 8: Store transaction in DB
// STEP 8: Store transaction in DB using txn_id (order ID)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Split name into first/last (simple split on first space)
$full_name = $user_data['name'] ?? '';
$name_parts = explode(' ', $full_name, 2);
$first_name = $name_parts[0] ?? '';
$last_name = $name_parts[1] ?? '';
// BUILD UP PARTNERHIERARCHY FROM USER
$partner_product = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
$sql = 'INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name,
address_street, address_city, address_state, address_zip, address_country, account_id, payment_method, created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
address_street, address_city, address_state, address_zip, address_country, account_id, payment_method, accounthierarchy, created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$mollie_payment_id,
$txn_id, // Use generated transaction ID, not Mollie payment ID
$final_price,
0, // 0 = pending
$user_data['email'] ?? '',
$user_data['first_name'] ?? '',
$user_data['last_name'] ?? '',
$user_data['address_street'] ?? '',
$user_data['address_city'] ?? '',
$user_data['address_state'] ?? '',
$user_data['address_zip'] ?? '',
$user_data['address_country'] ?? '',
$first_name,
$last_name,
$user_data['address'] ?? '',
$user_data['city'] ?? '',
'', // address_state (not collected)
$user_data['postal'] ?? '',
$user_data['country'] ?? '',
$serial_number,
0, // payment method
$partner_product,
date('Y-m-d H:i:s')
]);
// Get the database ID
$transaction_id = $pdo->lastInsertId();
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// STEP 9: Store transaction item with serial_number in item_options
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$item_options = json_encode([
'serial_number' => $serial_number,
'equipment_id' => $equipment_id,
'hw_version' => $hw_version
'hw_version' => $hw_version,
'mollie_payment_id' => $mollie_payment_id // Store Mollie payment ID in options
], JSON_UNESCAPED_UNICODE);
$sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created)
VALUES (?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$mollie_payment_id,
$transaction_id, // Use database transaction ID (not txn_id string, not mollie_payment_id)
$version_id,
$final_price,
1,

View File

@@ -14,12 +14,16 @@ $post_content = json_decode($input,true);
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
list($whereclause,$condition) = getWhereclauselvl2("software_licenses",$permission,$partner,'');
list($whereclause,$condition) = getWhereclauselvl2("products_software_licenses",$permission,$partner,'');
//SET PARAMETERS FOR QUERY
$id = $post_content['rowID'] ?? ''; //check for rowID
$command = ($id == '')? 'insert' : 'update'; //IF rowID = empty then INSERT
if (isset($post_content['delete'])){$command = 'delete';} //change command to delete
// Check for bulk creation
$is_bulk = isset($post_content['bulk']) && $post_content['bulk'] === true;
$date = date('Y-m-d H:i:s');
//CREATE EMPTY STRINGS
@@ -27,12 +31,90 @@ $clause = '';
$clause_insert ='';
$input_insert = '';
//------------------------------------------
// BULK LICENSE CREATION
//------------------------------------------
if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$profile,$permission,'C') === 1){
$version_id = $post_content['version_id'] ?? '';
$serials = $post_content['serials'] ?? [];
$transaction_id = $post_content['transaction_id'] ?? '';
$license_type = $post_content['license_type'] ?? 0;
$status = $post_content['status'] ?? 0;
if (empty($version_id) || empty($serials) || !is_array($serials)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid parameters for bulk creation']);
exit;
}
$accounthierarchy = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
// Prepare statement for bulk insert
$sql = 'INSERT INTO products_software_licenses (version_id, license_key, license_type, status, transaction_id, accounthierarchy, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$created_count = 0;
foreach ($serials as $serial) {
if (empty($serial)) continue;
// Generate UUID for license key
$license_key = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
try {
$stmt->execute([
$version_id,
$license_key,
$license_type,
$status,
$transaction_id,
$accounthierarchy,
$date,
$username
]);
// Assign license to equipment if serial number exists
$eq_sql = 'UPDATE equipment SET sw_version_license = ? WHERE serialnumber = ? AND accounthierarchy LIKE ?';
$eq_stmt = $pdo->prepare($eq_sql);
$eq_stmt->execute([$license_key, $serial, '%'.$partner->soldto.'%']);
$created_count++;
} catch (Exception $e) {
debuglog("Error creating license for serial $serial: " . $e->getMessage());
}
}
echo json_encode(['success' => true, 'created' => $created_count]);
exit;
}
//------------------------------------------
// SINGLE LICENSE CREATION OR UPDATE
//------------------------------------------
//ADD STANDARD PARAMETERS TO ARRAY BASED ON INSERT OR UPDATE
if ($command == 'update'){
$post_content['updated'] = $date;
$post_content['updatedby'] = $username;
}
elseif ($command == 'insert'){
// Generate UUID for license key if not provided
if (empty($post_content['license_key'])) {
$post_content['license_key'] = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
$post_content['created'] = $date;
$post_content['createdby'] = $username;
$post_content['accounthierarchy'] = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
@@ -44,8 +126,8 @@ else {
//CREATE NEW ARRAY AND MAP TO CLAUSE
if(isset($post_content) && $post_content!=''){
foreach ($post_content as $key => $var){
if ($key == 'submit' || $key == 'rowID'){
//do nothing
if ($key == 'submit' || $key == 'rowID' || $key == 'serial' || $key == 'bulk' || $key == 'serials'){
//do nothing - skip these fields
}
else {
$criterias[$key] = $var;
@@ -69,13 +151,28 @@ if ($command == 'update' && isAllowed('products_software_licenses',$profile,$per
$execute_input[] = $id;
$stmt = $pdo->prepare($sql);
$stmt->execute($execute_input);
echo json_encode(['success' => true]);
}
elseif ($command == 'insert' && isAllowed('products_software_licenses',$profile,$permission,'C') === 1){
//INSERT NEW ITEM
//INSERT NEW ITEM
$sql = 'INSERT INTO products_software_licenses ('.$clause_insert.') VALUES ('.$input_insert.')';
$stmt = $pdo->prepare($sql);
$stmt->execute($execute_input);
$new_license_id = $pdo->lastInsertId();
$license_key = $post_content['license_key'];
// Assign license to equipment if serial number provided
if (!empty($post_content['serial'])) {
$serial = $post_content['serial'];
$eq_sql = 'UPDATE equipment SET sw_version_license = ? WHERE serialnumber = ? AND accounthierarchy LIKE ?';
$eq_stmt = $pdo->prepare($eq_sql);
$eq_stmt->execute([$license_key, $serial, '%'.$partner->soldto.'%']);
}
echo json_encode(['success' => true, 'license_id' => $new_license_id, 'license_key' => $license_key]);
}
elseif ($command == 'delete' && isAllowed('products_software_licenses',$profile,$permission,'D') === 1){
@@ -85,6 +182,7 @@ elseif ($command == 'delete' && isAllowed('products_software_licenses',$profile,
//Add deletion to changelog
changelog($dbname,'products_software_licenses',$id,'Delete','Delete',$username);
echo json_encode(['success' => true]);
} else
{
//do nothing

View File

@@ -72,10 +72,9 @@ $hw_version = (isset($criterias['hw_version']))? $criterias['hw_version']:'';
if ($command == 'update' && isAllowed('products_software_versions',$profile,$permission,'U') === 1){
//REMOVE LATEST FLAG FROM OTHER WHEN SEND
//Max 2 latest flags per hw_version: 1 with price (has upgrade path with price) and 1 without
if (isset($criterias['latest']) && $criterias['latest'] == 1){
$sql = 'UPDATE products_software_versions SET latest = 0 WHERE hw_version = ? AND rowID != ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$hw_version, $id]);
updateSoftwareLatestFlags($pdo, $id, $hw_version);
}
$sql = 'UPDATE products_software_versions SET '.$clause.' WHERE rowID = ? ';
@@ -85,17 +84,17 @@ if ($command == 'update' && isAllowed('products_software_versions',$profile,$per
}
elseif ($command == 'insert' && isAllowed('products_software_versions',$profile,$permission,'C') === 1){
//REMOVE LATEST FLAG FROM OTHER IF SET
if (isset($criterias['latest']) && $criterias['latest'] == 1){
$sql = 'UPDATE products_software_versions SET latest = 0 WHERE hw_version = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$hw_version]);
}
//INSERT NEW ITEM
$sql = 'INSERT INTO products_software_versions ('.$clause_insert.') VALUES ('.$input_insert.')';
$stmt = $pdo->prepare($sql);
$stmt->execute($execute_input);
$new_id = $pdo->lastInsertId();
//REMOVE LATEST FLAG FROM OTHER IF SET
//Max 2 latest flags per hw_version: 1 with price (has upgrade path with price) and 1 without
if (isset($criterias['latest']) && $criterias['latest'] == 1){
updateSoftwareLatestFlags($pdo, $new_id, $hw_version);
}
}
elseif ($command == 'delete' && isAllowed('products_software_versions',$profile,$permission,'D') === 1){

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);
@@ -5543,3 +5528,48 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
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

View File

@@ -2920,3 +2920,50 @@ main .products .product .price, main .products .products-wrapper .product .price
height: 25px;
margin: 1px;
}
/* Button alignment styles */
.form-actions,
.modal-actions,
.dialog-actions,
.button-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
margin-top: 20px;
}
.title-actions {
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-end;
}
.filter-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
flex-wrap: wrap;
}
main .form .button-container,
main .form .form-actions,
main .content-block .button-container {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
}
.dialog .content .footer,
.modal .modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
padding: 20px;
border-top: 1px solid #eee;
}

View File

@@ -2922,3 +2922,50 @@ main .products .product .price, main .products .products-wrapper .product .price
height: 25px;
margin: 1px;
}
/* Button alignment styles */
.form-actions,
.modal-actions,
.dialog-actions,
.button-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
margin-top: 20px;
}
.title-actions {
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-end;
}
.filter-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
flex-wrap: wrap;
}
main .form .button-container,
main .form .form-actions,
main .content-block .button-container {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
}
.dialog .content .footer,
.modal .modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
padding: 20px;
border-top: 1px solid #eee;
}

View File

@@ -127,6 +127,7 @@ $view = '
<div class="content-title responsive-flex-wrap responsive-pad-bot-3">
<h2 class="responsive-width-100">'.$view_asset_h2.' - '.$responses->equipmentID.'</h2>
<a href="index.php?page='.$_SESSION['origin'].'&p='.$_SESSION['p'].$_SESSION['status'].$_SESSION['sort'].$_SESSION['search'].$_SESSION['firmware'].$_SESSION['servicedate'].$_SESSION['warrantydate'].$_SESSION['partnerid'].'" class="btn alt mar-right-2">←</a>
<a href="index.php?page=history&equipmentID='.$responses->equipmentID.'" class="btn"><i class="fa-solid fa-clock-rotate-left"></i></a>
';
//------------------------------------
@@ -395,7 +396,7 @@ $view .= '<div class="content-block">
<div class="block-header">
<i class="fa-solid fa-bars fa-sm"></i>'.$view_asset_actions.'
</div>
<a href="index.php?page=history&equipmentID='.$responses->equipmentID.'" class="btn">'.$button_history.'</a>
'.$view_communication.'
'.$view_users.'
';

606
licenses.php Normal file
View File

@@ -0,0 +1,606 @@
<?php
defined(page_security_key) or exit;
if (debug && debug_id == $_SESSION['id']){
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
}
include_once './assets/functions.php';
include_once './settings/settings_redirector.php';
//SET ORIGIN FOR NAVIGATION
$prev_page = $_SESSION['prev_origin'] ?? '';
$page = $_SESSION['origin'] = 'licenses';
//Check if allowed
if (isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'R') === 0){
header('location: index.php');
exit;
}
//PAGE Security
$page_manage = 'licenses';
$update_allowed = isAllowed($page_manage ,$_SESSION['profile'],$_SESSION['permission'],'U');
$delete_allowed = isAllowed($page_manage ,$_SESSION['profile'],$_SESSION['permission'],'D');
$create_allowed = isAllowed($page_manage ,$_SESSION['profile'],$_SESSION['permission'],'C');
// Handle license status update
if ($update_allowed === 1 && isset($_POST['submit'])) {
$data = json_encode($_POST, JSON_UNESCAPED_UNICODE);
$responses = ioServer('/v2/products_software_licenses', $data);
if ($responses !== 'NOK'){
header('Location: index.php?page=licenses&success_msg=2');
exit;
}
}
//GET PARAMETERS
$pagination_page = isset($_GET['p']) ? $_GET['p'] : 1;
$status = isset($_GET['status']) ? '&status='.$_GET['status'] : '';
$search = isset($_GET['search']) ? '&search='.$_GET['search'] : '';
// Determine the URL
$url = 'index.php?page=licenses'.$status.$search;
//GET Details from URL
$GET_VALUES = urlGETdetails($_GET) ?? '';
//CALL TO API
$api_url = '/v2/products_software_licenses/'.$GET_VALUES;
$responses = ioServer($api_url,'');
//Decode Payload
if (!empty($responses)){$responses = json_decode($responses);}else{$responses = null;}
//Return QueryTotal from API
$api_url = '/v2/products_software_licenses/'.$GET_VALUES.'&totals=';
$query_total = ioServer($api_url,'');
//Decode Payload
if (!empty($query_total)){$query_total = json_decode($query_total,);}else{$query_total = null;}
// Handle success messages
if (isset($_GET['success_msg'])) {
if ($_GET['success_msg'] == 1) {
$success_msg = 'Licenses created successfully!';
}
if ($_GET['success_msg'] == 2) {
$success_msg = 'License updated successfully!';
}
if ($_GET['success_msg'] == 3) {
$success_msg = 'License deleted successfully!';
}
}
// Get software versions for dropdown
$api_url = '/v2/products_software_versions/list=&status=1';
$software_versions = ioServer($api_url,'');
if (!empty($software_versions)){$software_versions = json_decode($software_versions);}else{$software_versions = null;}
template_header('Licenses', 'licenses','view');
$view = '
<div class="content-title">
<div class="title">
<i class="fa-solid fa-key"></i>
<div class="txt">
<h2>Software Licenses ('.$query_total.')</h2>
<p>Manage and create software licenses for devices</p>
</div>
</div>
<div class="title-actions">';
if ($create_allowed === 1){
$view .= '<button onclick="openBulkLicenseModal()" class="btn">+</button>';
}
$view .= '<button id="filter-toggle" class="btn alt" onclick="toggleFilters()">
<i class="fa-solid fa-search"></i>
</button>
</div>
</div>';
if (isset($success_msg)){
$view .= ' <div class="msg success">
<i class="fas fa-check-circle"></i>
<p>'.$success_msg.'</p>
<i class="fas fa-times"></i>
</div>';
}
$view .= '
<div id="filter-panel" class="filter-panel" style="display: none;">
<div class="filter-content">
<form action="" method="get">
<input type="hidden" name="page" value="licenses">
<div class="filter-row">
<div class="filter-group">
<select name="status">
<option value="" disabled selected>Status</option>
<option value="0"'.($status==0?' selected':'').'>Inactive</option>
<option value="1"'.($status==1?' selected':'').'>Assigned</option>
<option value="2"'.($status==2?' selected':'').'>Expired</option>
</select>
</div>
<div class="filter-group search-group">
<input type="text" name="search" placeholder="Search license key..." value="">
</div>
</div>
<div class="filter-actions">
<button type="submit" class="btn"><i class="fas fa-level-down-alt fa-rotate-90"></i></button>
<a class="btn alt" href="index.php?page=licenses">Clear</a>
</div>
</form>
</div>
</div>
';
$view .= '
<div class="content-block">
<div class="table">
<table class="sortable">
<thead>
<tr>
<th>License Key</th>
<th>Software Version</th>
<th>Status</th>
<th>Transaction ID</th>
<th>Starts At</th>
<th>Expires</th>
<th>Assigned To (Serial)</th>
</tr>
</thead>
<tbody>
';
if (empty($responses)){
$view .= '
<tr>
<td colspan="7" style="text-align:center;">No licenses found</td>
</tr>';
}
else {
foreach ($responses as $response){
// Check if license is expired based on timestamp
$actual_status = $response->status;
if (!empty($response->expires_at)) {
$expiry_time = strtotime($response->expires_at);
$current_time = time();
if ($current_time > $expiry_time) {
// License is expired - override status
$actual_status = 2;
}
}
// Status display based on actual status
$status_text = '';
if ($actual_status == 0) {
$status_text = 'Inactive';
} elseif ($actual_status == 1) {
$status_text = 'Assigned';
} elseif ($actual_status == 2) {
$status_text = 'Expired';
}
// Format dates
$starts_display = '-';
if (!empty($response->starts_at)) {
$starts_display = date('Y-m-d', strtotime($response->starts_at));
}
$expires_display = '-';
if (!empty($response->expires_at)) {
$expires_display = date('Y-m-d', strtotime($response->expires_at));
}
$view .= '
<tr style="cursor: pointer;" onclick="openLicenseModal('.htmlspecialchars(json_encode($response), ENT_QUOTES).')">
<td>'.$response->license_key.'</td>
<td>'.$response->version_name.'</td>
<td><span class="status id'.$actual_status.'">'.$status_text.'</span></td>
<td>'.($response->transaction_id ?? '-').'</td>
<td>'.$starts_display.'</td>
<td>'.$expires_display.'</td>
<td>'.($response->assigned_serial ?? '-').'</td>
</tr>
';
}
}
$view .= '
</tbody>
</table>
</div>
</div>
';
$view.='<div class="pagination">';
if ($pagination_page > 1) {
$page = $pagination_page-1;
$view .= '<a href="'.$url.'&p=1">First</a>';
$view .= '<a href="'.$url.'&p='.$page.'">Prev</a>';
}
$totals = ceil($query_total / 50) == 0 ? 1 : ceil($query_total / 50);
$view .= '<span> Page '.$pagination_page.' of '.$totals.'</span>';
if ($pagination_page * 50 < $query_total){
$page = $pagination_page+1;
$view .= '<a href="'.$url.'&p='.$page.'">Next</a>';
$view .= '<a href="'.$url.'&p='.$totals.'">Last</a>';
}
$view .= '</div>';
// Bulk License Modal
$view .= '
<div id="bulkLicenseModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h3><i class="fa-solid fa-key"></i> Create Bulk Licenses</h3>
<button onclick="closeBulkLicenseModal()" class="close-btn">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="modal-body">
<form id="bulkLicenseForm">
<div class="form-group">
<label for="bulk_version_id">Software Version *</label>
<select id="bulk_version_id" name="version_id" required>
<option value="">Select software version...</option>';
if (!empty($software_versions)) {
foreach ($software_versions as $version) {
$view .= '<option value="'.$version->rowID.'">'.$version->name.' (v'.$version->version.')</option>';
}
}
$view .= '
</select>
</div>
<div class="form-group">
<label for="bulk_serials">Serial Numbers *</label>
<textarea id="bulk_serials" name="serials" rows="8" placeholder="Paste serial numbers, one per line:&#10;SN001&#10;SN002&#10;SN003" required></textarea>
<small style="color: #666; display: block; margin-top: 5px;">Enter one serial number per line</small>
</div>
<div class="form-group">
<label for="bulk_transaction_id">Transaction ID *</label>
<input type="text" id="bulk_transaction_id" name="transaction_id" placeholder="e.g., PO-12345" required>
</div>
<div class="form-actions">
<button type="button" onclick="closeBulkLicenseModal()" class="btn alt">Cancel</button>
<button type="submit" class="btn">Create Licenses</button>
</div>
</form>
</div>
</div>
</div>';
// License Details Modal
$view .= '
<div id="licenseDetailModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h3><i class="fa-solid fa-key"></i> License Details</h3>
<button onclick="closeLicenseDetailModal()" class="close-btn">
<i class="fa-solid fa-times"></i>
</button>
</div>
<div class="modal-body">
<form action="" method="post" id="licenseDetailForm">
<div class="license-details-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 25px;">
<div class="detail-group">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">License Key</label>
<div id="detail_license_key" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-family: monospace; font-size: 14px; word-break: break-all;"></div>
</div>
<div class="detail-group">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">Software Version</label>
<div id="detail_version_name" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-size: 14px;"></div>
</div>
<div class="detail-group">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">Status</label>
<div id="detail_status" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-size: 14px;"></div>
</div>
<div class="detail-group">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">Transaction ID</label>
<div id="detail_transaction_id" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-size: 14px;"></div>
</div>
<div class="detail-group">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">Starts At</label>
<div id="detail_starts_at" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-size: 14px;"></div>
</div>
<div class="detail-group">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">Expires</label>
<div id="detail_expires_at" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-size: 14px;"></div>
</div>
<div class="detail-group" style="grid-column: 1 / -1;">
<label style="display: block; margin-bottom: 5px; color: #666; font-size: 13px; font-weight: 500;">Assigned To (Serial)</label>
<div id="detail_assigned_serial" style="padding: 10px 12px; background: #f8f9fa; border-radius: 4px; font-size: 14px;"></div>
</div>
</div>
<input type="hidden" id="detail_rowID" name="rowID" value="">
<input type="hidden" name="status" value="0">
<div class="form-actions" style="border-top: 1px solid #e0e0e0; padding-top: 20px;">';
if ($update_allowed === 1) {
$view .= '
<button type="submit" name="submit" id="setInactiveBtn" class="btn" style="background: #dc3545;" onclick="return confirm(\'Are you sure you want to set this license as inactive?\')">
<i class="fa-solid fa-ban"></i> Set Inactive
</button>';
}
$view .= '
<button type="button" onclick="closeLicenseDetailModal()" class="btn alt">Close</button>
</div>
</form>
</div>
</div>
</div>';
//OUTPUT
echo $view;
// Add JavaScript for modals and API calls
echo '
<script>
// Store current license data
let currentLicenseData = null;
// Modal functions
function openBulkLicenseModal() {
document.getElementById("bulkLicenseModal").style.display = "flex";
}
function closeBulkLicenseModal() {
document.getElementById("bulkLicenseModal").style.display = "none";
document.getElementById("bulkLicenseForm").reset();
}
// License detail modal functions
function openLicenseModal(licenseData) {
currentLicenseData = licenseData;
// Calculate actual status (check expiry)
let actualStatus = licenseData.status;
if (licenseData.expires_at) {
const expiryTime = new Date(licenseData.expires_at).getTime();
const currentTime = new Date().getTime();
if (currentTime > expiryTime) {
actualStatus = 2;
}
}
// Status text and formatting
let statusText = "";
let statusClass = "";
if (actualStatus == 0) {
statusText = "Inactive";
statusClass = "id0";
} else if (actualStatus == 1) {
statusText = "Assigned";
statusClass = "id1";
} else if (actualStatus == 2) {
statusText = "Expired";
statusClass = "id2";
}
// Format dates
const startsDisplay = licenseData.starts_at ? new Date(licenseData.starts_at).toLocaleDateString() : "-";
const expiresDisplay = licenseData.expires_at ? new Date(licenseData.expires_at).toLocaleDateString() : "-";
// Populate modal fields
document.getElementById("detail_license_key").textContent = licenseData.license_key || "-";
document.getElementById("detail_version_name").textContent = licenseData.version_name || "-";
document.getElementById("detail_status").innerHTML = \'<span class="status \' + statusClass + \'">\' + statusText + \'</span>\';
document.getElementById("detail_transaction_id").textContent = licenseData.transaction_id || "-";
document.getElementById("detail_starts_at").textContent = startsDisplay;
document.getElementById("detail_expires_at").textContent = expiresDisplay;
document.getElementById("detail_assigned_serial").textContent = licenseData.assigned_serial || "-";
// Set hidden form field
document.getElementById("detail_rowID").value = licenseData.rowID || "";
// Show/hide inactive button based on current status
const inactiveBtn = document.getElementById("setInactiveBtn");
if (inactiveBtn) {
if (actualStatus == 0) {
inactiveBtn.style.display = "none";
} else {
inactiveBtn.style.display = "inline-block";
}
}
document.getElementById("licenseDetailModal").style.display = "flex";
}
function closeLicenseDetailModal() {
document.getElementById("licenseDetailModal").style.display = "none";
currentLicenseData = null;
}
// Close modal on background click
window.onclick = function(event) {
const bulkModal = document.getElementById("bulkLicenseModal");
const detailModal = document.getElementById("licenseDetailModal");
if (event.target == bulkModal) {
closeBulkLicenseModal();
}
if (event.target == detailModal) {
closeLicenseDetailModal();
}
}
// Bulk license form submission
document.getElementById("bulkLicenseForm").addEventListener("submit", async function(e) {
e.preventDefault();
const serials = document.getElementById("bulk_serials").value
.split("\\n")
.map(s => s.trim())
.filter(s => s.length > 0);
if (serials.length === 0) {
alert("Please enter at least one serial number.");
return;
}
const formData = {
bulk: true,
version_id: document.getElementById("bulk_version_id").value,
serials: serials,
transaction_id: document.getElementById("bulk_transaction_id").value,
license_type: 0,
status: 0
};
try {
const response = await fetch("api.php/v2/products_software_licenses", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + sessionStorage.getItem("token")
},
body: JSON.stringify(formData)
});
if (response.ok) {
window.location.href = "index.php?page=licenses&success_msg=1";
} else {
alert("Error creating licenses. Please try again.");
}
} catch (error) {
console.error("Error:", error);
alert("Error creating licenses. Please try again.");
}
});
// Filter toggle function
function toggleFilters() {
const panel = document.getElementById("filter-panel");
if (panel.style.display === "none") {
panel.style.display = "block";
} else {
panel.style.display = "none";
}
}
</script>
<style>
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
align-items: center;
justify-content: center;
}
.modal-content {
background-color: #fff;
margin: auto;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
width: 90%;
max-width: 600px;
}
.modal-header {
padding: 20px 25px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
color: #333;
font-size: 20px;
}
.close-btn {
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 25px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.form-group input[type="text"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-group textarea {
resize: vertical;
font-family: monospace;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #04AA6D;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 25px;
}
</style>
';
template_footer();
?>

View File

@@ -47,6 +47,12 @@ $main_menu = [
"icon" => "fas fa-tachometer-alt",
"name" => "menu_sales_orders"
],
"licenses" => [
"url" => "licenses",
"selected" => "licenses",
"icon" => "fas fa-tachometer-alt",
"name" => "menu_sales_licenses"
],
"identity" => [
"url" => "identity",
"selected" => "identity",

View File

@@ -6,7 +6,7 @@ define('superuser_profile','admin,dashboard,profile,application,assets,firmwaret
/*Admin*/
define('admin_profile','account,accounts,admin,dashboard,profile,application,assets,buildtool,buildtool,cartest,cartest_manage,cartests,changelog,communication,communication_send,communications,firmwaretool,histories,history,history_manage,marketing,partner,partners,sales,servicereport,servicereports,contract,contract_manage,contracts,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,report_build,report_contracts_billing,report_healthindex,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_manage,users');
/*AdminPlus*/
define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,firmwaretool,generate_download_token,histories,history,history_manage,identity,identity_dealers,invoice,language,logfile,mailer,maintenance,marketing,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users');
define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,firmwaretool,generate_download_token,histories,history,history_manage,identity,identity_dealers,language,licenses,logfile,mailer,maintenance,marketing,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,payment,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,softwaretool,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users');
/*Build*/
define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software');
/*Commerce*/
@@ -18,7 +18,7 @@ define('firmware','application,firmwaretool,products_software');
/*Garage*/
define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions');
/*Interface*/
define('interface','application,firmwaretool,contract,contracts,equipment_manage,equipments,products_software,products_versions,users');
define('interface','application,firmwaretool,invoice,payment,transactions,transactions_items,contract,contracts,equipment_manage,equipments,products_software,products_versions,users');
/*Service*/
define('service','admin,dashboard,profile,application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,servicereport,servicereports,equipment,equipment_manage,equipments,products_software,user,user_manage,users');
/*Other*/

View File

@@ -55,8 +55,10 @@ $all_views = [
"history_manage",
"identity",
"identity_dealers",
"initialize",
"invoice",
"language",
"licenses",
"logfile",
"mailer",
"maintenance",
@@ -69,6 +71,7 @@ $all_views = [
"orders",
"partner",
"partners",
"payment",
"placeorder",
"pricelists",
"pricelists_items",
@@ -120,6 +123,7 @@ $all_views = [
"software_available",
"software_download",
"software_update",
"softwaretool",
"tax",
"taxes",
"test",
@@ -136,6 +140,7 @@ $all_views = [
"user_manage",
"users",
"vin",
"webhook_mollie",
];
?>

View File

@@ -18,24 +18,73 @@ $bearertoken = createCommunicationToken($_SESSION['userkey']);
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// PAYMENT RETURN DETECTION
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payment_return = isset($_GET['payment_id']) ? $_GET['payment_id'] : null;
$payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : null;
$payment_return_status = isset($_GET['payment_return']) ? $_GET['payment_return'] : null;
template_header('Softwaretool', 'softwaretool','view');
// Show payment return message if returning from payment
$view = '';
$payment_modal = '';
if ($payment_return && $payment_return_status) {
$view = '
<div class="content-title">
<div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 15px; border-radius: 6px; margin-bottom: 20px;">
<i class="fa-solid fa-check-circle"></i>
<strong>Payment Successful!</strong>
<p style="margin: 10px 0 0 0;">Your payment has been processed. Please reconnect your device to apply the software upgrade.</p>
<p style="margin: 5px 0 0 0; font-size: 12px; color: #666;">Payment ID: '.htmlspecialchars($payment_return).'</p>
</div>
</div>';
} else {
$view = '';
// Check actual payment status in database
$pdo = dbConnect($dbname);
$sql = 'SELECT payment_status FROM transactions WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$payment_return]);
$transaction = $stmt->fetch(PDO::FETCH_ASSOC);
if ($transaction) {
if ($transaction['payment_status'] == 1) {
// Payment confirmed as paid
$payment_modal = '
<div id="paymentModal" class="modal" style="display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div class="modal-content" style="background: white; border-radius: 12px; max-width: 500px; margin: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); position: relative;">
<span class="close" onclick="closePaymentModal()" style="position: absolute; top: 15px; right: 20px; font-size: 28px; font-weight: bold; color: #999; cursor: pointer;">&times;</span>
<div style="text-align: center; padding: 40px 30px;">
<i class="fa-solid fa-check-circle" style="font-size: 64px; color: #28a745; margin-bottom: 20px;"></i>
<h2 style="color: #155724; margin-bottom: 15px;">Payment Successful!</h2>
<p style="margin-bottom: 10px; color: #333;">Your payment has been processed. Please reconnect your device to apply the software upgrade.</p>
<p style="font-size: 12px; color: #666; margin-bottom: 25px;">Order ID: '.htmlspecialchars($payment_return).'</p>
<button onclick="closePaymentModal()" class="btn" style="padding: 12px 30px;">Continue</button>
</div>
</div>
</div>';
} else if ($transaction['payment_status'] == 0 || $transaction['payment_status'] == 101) {
// Payment pending
$payment_modal = '
<div id="paymentModal" class="modal" style="display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div class="modal-content" style="background: white; border-radius: 12px; max-width: 500px; margin: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.3);">
<div style="text-align: center; padding: 40px 30px;">
<i class="fa-solid fa-clock" style="font-size: 64px; color: #ffc107; margin-bottom: 20px;"></i>
<h2 style="color: #856404; margin-bottom: 15px;">Payment Processing...</h2>
<p style="margin-bottom: 10px; color: #333;">Your payment is being processed. This page will update automatically when confirmed.</p>
<p style="font-size: 12px; color: #666; margin-bottom: 25px;">Order ID: '.htmlspecialchars($payment_return).'</p>
<i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: #ffc107;"></i>
</div>
</div>
</div>
<script>
// Auto-refresh every 3 seconds to check payment status
setTimeout(function() { location.reload(); }, 3000);
</script>';
} else {
// Payment failed/cancelled
$payment_modal = '
<div id="paymentModal" class="modal" style="display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div class="modal-content" style="background: white; border-radius: 12px; max-width: 500px; margin: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); position: relative;">
<span class="close" onclick="closePaymentModal()" style="position: absolute; top: 15px; right: 20px; font-size: 28px; font-weight: bold; color: #999; cursor: pointer;">&times;</span>
<div style="text-align: center; padding: 40px 30px;">
<i class="fa-solid fa-exclamation-circle" style="font-size: 64px; color: #dc3545; margin-bottom: 20px;"></i>
<h2 style="color: #721c24; margin-bottom: 15px;">Payment Failed</h2>
<p style="margin-bottom: 10px; color: #333;">Your payment could not be processed. Please try again.</p>
<p style="font-size: 12px; color: #666; margin-bottom: 25px;">Order ID: '.htmlspecialchars($payment_return).'</p>
<button onclick="closePaymentModal()" class="btn" style="padding: 12px 30px;">Close</button>
</div>
</div>
</div>';
}
}
}
$view .= '
@@ -90,7 +139,7 @@ $view .= '<div class="content-block">
<div id="softwareOptions" style="margin-top: 20px; display:none;">
<h3 style="margin-bottom: 20px; color: #333;">'.$softwaretool_select_upgrade.'</h3>
<div id="softwareOptionsGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;">
<div id="softwareOptionsGrid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; justify-content: center; max-width: 1200px; margin: 0 auto;">
</div>
</div>
@@ -144,6 +193,9 @@ $view .= '</div>';
//OUTPUT
echo $view;
// Output payment modal if exists
echo $payment_modal;
echo '
<script src="assets/upload.js?'.script_version.'"></script>
@@ -169,12 +221,29 @@ echo '
}
};
// Payment modal functions
window.closePaymentModal = function() {
const modal = document.getElementById("paymentModal");
if (modal) {
modal.style.display = "none";
// Clean URL by removing payment_return and order_id parameters
const url = new URL(window.location);
url.searchParams.delete("payment_return");
url.searchParams.delete("order_id");
window.history.replaceState({}, document.title, url);
}
};
// Close modal on background click
document.addEventListener("click", function(e) {
const modal = document.getElementById("helpModal");
if (modal && e.target === modal) {
const helpModal = document.getElementById("helpModal");
if (helpModal && e.target === helpModal) {
closeInstructions();
}
const paymentModal = document.getElementById("paymentModal");
if (paymentModal && e.target === paymentModal) {
closePaymentModal();
}
});
</script>';

View File

@@ -2937,6 +2937,7 @@ main .products .product .price, main .products .products-wrapper .product .price
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-end;
}
.filter-panel {
@@ -2979,6 +2980,7 @@ main .products .product .price, main .products .products-wrapper .product .price
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
flex-wrap: wrap;
}
@@ -3085,3 +3087,45 @@ main .products .product .price, main .products .products-wrapper .product .price
color: #6c757d;
font-style: italic;
}
/* Button alignment styles */
.form-actions,
.modal-actions,
.dialog-actions,
.button-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
margin-top: 20px;
}
/* Ensure title-actions stay right-aligned */
.title-actions {
display: flex;
gap: 10px;
align-items: center;
justify-content: flex-end;
}
/* Form button containers should be right-aligned */
main .form .button-container,
main .form .form-actions,
main .content-block .button-container {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
}
/* Right-align buttons in dialog footers */
.dialog .content .footer,
.modal .modal-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
padding: 20px;
border-top: 1px solid #eee;
}

View File

@@ -5,14 +5,22 @@
require_once 'settings/config_redirector.php';
require_once 'assets/functions.php';
include dirname(__FILE__).'/settings/settings_redirector.php';
// DEBUG: Log webhook call
debuglog("WEBHOOK CALLED - POST data: " . print_r($_POST, true));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//LOGIN TO API (same as commerce webhook.php)
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$data = json_encode(array("clientID" => software_update_user, "clientsecret" => software_update_pw), JSON_UNESCAPED_UNICODE);
debuglog("WEBHOOK: Attempting API authorization...");
debuglog("WEBHOOK: Interface user: " . interface_user);
$data = json_encode(array("clientID" => interface_user, "clientsecret" => interface_pw), JSON_UNESCAPED_UNICODE);
$responses = ioAPIv2('/v2/authorization', $data,'');
debuglog("WEBHOOK: Authorization response: " . $responses);
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
$clientsecret = $responses['token'];
debuglog("WEBHOOK: Token obtained: " . ($clientsecret ? 'YES' : 'NO'));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// BASEURL is required for invoice template
@@ -64,7 +72,16 @@ try {
// PRODUCTION MODE - Retrieve the payment's current state from Mollie
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payment = $mollie->payments->get($_POST["id"]);
$orderId = $payment->metadata->order_id;
// Get order ID from metadata (same as commerce product)
$orderId = $payment->metadata->order_id ?? null;
if (!$orderId) {
debuglog("WEBHOOK ERROR: No order_id in payment metadata");
http_response_code(400);
exit;
}
debuglog("WEBHOOK: Payment ID: {$payment->id}, Order ID: {$orderId}");
$payment_status = null;
if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) {
@@ -81,18 +98,33 @@ try {
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Update transaction status via API
// Update transaction status directly in database
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment_status !== null) {
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => $payment_status), JSON_UNESCAPED_UNICODE);
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
$transaction = json_decode($transaction,true);
debuglog("WEBHOOK: Order ID: $orderId, Payment Status: $payment_status");
$pdo = dbConnect($dbname);
// Update transaction status
$sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$payment_status, $orderId]);
debuglog("WEBHOOK: Transaction status updated in database");
// Fetch transaction data for license creation
$sql = 'SELECT * FROM transactions WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$transaction = $stmt->fetch(PDO::FETCH_ASSOC);
debuglog("WEBHOOK: Transaction data: " . print_r($transaction, true));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Only create license and invoice if payment is PAID
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($payment_status == 1 && $transaction !== null && !empty($transaction)) {
if(count($transaction) > 0) {
debuglog("WEBHOOK: Payment is PAID, processing license...");
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// CREATE LICENSE for software upgrade
@@ -100,9 +132,10 @@ try {
$pdo = dbConnect($dbname);
// Fetch transaction items to find software upgrade
// Note: transactions_items.txn_id is the database ID (transaction.id), not the txn_id string
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$orderId]);
$stmt->execute([$transaction['id']]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
@@ -124,14 +157,14 @@ try {
// Create license
$sql = 'INSERT INTO products_software_licenses
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
(version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$item['item_id'], // version_id
1, // license_type (1 = upgrade)
$license_key,
$options['equipment_id'],
'upgrade',
1, // active
1, // status = active
date('Y-m-d H:i:s'),
'2099-12-31 23:59:59', // effectively permanent
$orderId,
@@ -139,26 +172,79 @@ try {
'webhook' // created by webhook
]);
debuglog("WEBHOOK: License created: $license_key");
// Update equipment.sw_version_license
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$license_key, $options['equipment_id']]);
debuglog("WEBHOOK: Equipment updated with license: {$options['equipment_id']}");
} else {
debuglog("WEBHOOK: License already exists for order: $orderId");
}
} else {
debuglog("WEBHOOK: Not a software upgrade item (no serial_number/equipment_id)");
}
} else {
debuglog("WEBHOOK: No item_options found");
}
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
// Generate INVOICE via API
// Generate INVOICE directly in database
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$payload = json_encode(array("txn_id" => $transaction['transaction_id']), JSON_UNESCAPED_UNICODE);
$invoice = ioAPIv2('/v2/invoice/',$payload,$clientsecret);
$invoice = json_decode($invoice,true);
if ($invoice !== null && !empty($invoice)) {
// Fetch full invoice data with customer details
$invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret);
$invoice_cust = json_decode($invoice_cust,true);
// Check if invoice already exists for this transaction
$sql = 'SELECT id FROM invoice WHERE txn_id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$transaction['txn_id']]);
$existing_invoice = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$existing_invoice) {
// Create invoice
$sql = 'INSERT INTO invoice (txn_id, payment_status, payment_amount, shipping_amount, discount_amount, tax_amount, created)
VALUES (?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
$transaction['txn_id'],
$transaction['payment_status'],
$transaction['payment_amount'],
$transaction['shipping_amount'] ?? 0.00,
$transaction['discount_amount'] ?? 0.00,
$transaction['tax_amount'] ?? 0.00,
date('Y-m-d H:i:s')
]);
$invoice_id = $pdo->lastInsertId();
debuglog("WEBHOOK: Invoice created with ID: $invoice_id");
} else {
$invoice_id = $existing_invoice['id'];
debuglog("WEBHOOK: Invoice already exists with ID: $invoice_id");
}
// Fetch full invoice data with customer details for email
// Note: invoice.txn_id = transactions.txn_id (VARCHAR order ID)
// transactions_items.txn_id = transactions.id (INT database ID)
$sql = 'SELECT tx.*, txi.item_id, txi.item_price, txi.item_quantity, txi.item_options,
p.productcode, p.productname, inv.id as invoice, inv.created as invoice_created,
i.language as user_language
FROM invoice inv
LEFT JOIN transactions tx ON tx.txn_id = inv.txn_id
LEFT JOIN transactions_items txi ON txi.txn_id = tx.id
LEFT JOIN products p ON p.rowID = txi.item_id
LEFT JOIN identity i ON i.userkey = tx.account_id
WHERE inv.id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$invoice_id]);
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
debuglog("WEBHOOK: Invoice data fetched: " . print_r($invoice_data, true));
if (!empty($invoice_data)) {
debuglog("WEBHOOK: Transforming invoice data...");
// Transform the data (group items like the API does)
$invoice_cust = transformOrderData($invoice_data);
debuglog("WEBHOOK: Transformed invoice data: " . print_r($invoice_cust, true));
// Determine invoice language
if (!empty($invoice_cust['customer']['language'])) {
@@ -170,28 +256,44 @@ try {
}
// Generate invoice HTML (using custom template for software upgrades)
debuglog("WEBHOOK: Calling generateSoftwareInvoice with language: $invoice_language");
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
debuglog("WEBHOOK: Invoice generated - Customer email: $customer_email, Order ID: $order_id");
debuglog("WEBHOOK: Invoice HTML length: " . strlen($data));
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//CREATE PDF using DomPDF
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
debuglog("WEBHOOK: Creating PDF...");
$dompdf->loadHtml($data);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$subject = 'Software Upgrade - Invoice: '.$order_id;
$attachment = $dompdf->output();
debuglog("WEBHOOK: PDF created, size: " . strlen($attachment) . " bytes");
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
//Send email via PHPMailer
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
send_mail($customer_email, $subject, $data, $attachment, $subject);
debuglog("WEBHOOK: Attempting to send email to: $customer_email");
debuglog("WEBHOOK: Email subject: $subject");
debuglog("WEBHOOK: Email config - Host: " . (defined('email_host_name') ? email_host_name : 'NOT DEFINED'));
debuglog("WEBHOOK: Email config - Port: " . (defined('email_outgoing_port') ? email_outgoing_port : 'NOT DEFINED'));
debuglog("WEBHOOK: Email config - Security: " . (defined('email_outgoing_security') ? email_outgoing_security : 'NOT DEFINED'));
debuglog("WEBHOOK: Email config - Username: " . (defined('email') ? email : 'NOT DEFINED'));
// The send_mail function will exit on error and debuglog the error
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
debuglog("WEBHOOK: Email sent successfully to: $customer_email");
// Send to bookkeeping if configured
if(invoice_bookkeeping){
debuglog("WEBHOOK: Sending to bookkeeping: " . email_bookkeeping);
send_mail(email_bookkeeping, $subject, $data, $attachment, $subject);
}
} else {
debuglog("WEBHOOK: No invoice data found for invoice_id: $invoice_id");
}
}
}
}
@@ -200,9 +302,13 @@ try {
echo "OK";
} catch (\Mollie\Api\Exceptions\ApiException $e) {
debuglog("WEBHOOK ERROR (Mollie API): " . $e->getMessage());
debuglog("WEBHOOK ERROR TRACE: " . $e->getTraceAsString());
error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
} catch (Exception $e) {
debuglog("WEBHOOK ERROR (General): " . $e->getMessage());
debuglog("WEBHOOK ERROR TRACE: " . $e->getTraceAsString());
error_log("Webhook error: " . htmlspecialchars($e->getMessage()));
http_response_code(500);
}