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