//Dropdown
-$partner_data = json_decode($_SESSION['partnerhierarchy']);
-$soldto_dropdown = listPartner('soldto',$_SESSION['permission'],$accounthierarchy->soldto,'');
+$partner_data = json_decode($_SESSION['authorization']['partnerhierarchy']);
+$soldto_dropdown = listPartner('soldto',$_SESSION['authorization']['permission'],$accounthierarchy->soldto,'');
$view .= '
'.$tab3.'
diff --git a/accounts.php b/accounts.php
index 72e748c..00c6705 100644
--- a/accounts.php
+++ b/accounts.php
@@ -1,7 +1,7 @@
getMessage() . " in " . $exception->getFile() . " on line " . $exception->getLine());
+ });
+}
+
//------------------------------------------
// Header security - enabled via config
//------------------------------------------
@@ -122,7 +134,7 @@ if($is_jwt_valid && str_contains($version, 'v')) {
//------------------------------------------
// Check for maintenance mode, exclude debug user
//------------------------------------------
- if(maintenance_mode == false|| debug_id == $user_data['id']){
+ if(maintenance_mode == false || debug_id == $user_data['id']){
//------------------------------------------
// Build up version and check if file is available
@@ -154,7 +166,8 @@ if($is_jwt_valid && str_contains($version, 'v')) {
// First check if endPoint is fileUpload
//------------------------------------------
$fileUploadEndpoints = [
- 'media_upload'
+ 'media_upload',
+ 'marketing_upload'
];
$isFileUploadEndpoint = in_array($collection, $fileUploadEndpoints);
diff --git a/api/.DS_Store b/api/.DS_Store
index 25bea50..362cceb 100644
Binary files a/api/.DS_Store and b/api/.DS_Store differ
diff --git a/api/v0/get/user_credentials.php b/api/v0/get/user_credentials.php
index 09062c9..6168639 100644
--- a/api/v0/get/user_credentials.php
+++ b/api/v0/get/user_credentials.php
@@ -15,7 +15,7 @@ $user_data = $stmt->fetch();
//Define User data
$partnerhierarchy = $user_data['partnerhierarchy'];
$permission = userRights($user_data['view']);
-$profile= getProfile($user_data['settings'],$permission);
+$profile= getUserPermissions($pdo, $user_data['id']);
$username = $user_data['username'];
$useremail = $user_data['email'];
$servicekey = $user_data['service'];
diff --git a/api/v0/post/application.php b/api/v0/post/application.php
index 6a8a550..323d616 100644
--- a/api/v0/post/application.php
+++ b/api/v0/post/application.php
@@ -56,7 +56,9 @@ if (!empty($post_content['sn']) && !empty($post_content['testdetails'])) {
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
$user = $username;
$account = $partnerhierarchy; //string
- $current_date = date("Y-m-d");
+ $service_date = date("Y-m-d", strtotime("+" . SERVICE_MONTHS . " months"));
+ $warranty_date = date("Y-m-d", strtotime("+" . WARRANTY_MONTHS . " months"));
+ $order_send_date = date("Y-m-d");
$input_type = $post_content['type'];
$testdetails = json_encode($post_content['testdetails']);
$serial = $post_content['sn'];
@@ -187,9 +189,9 @@ if (!empty($post_content['sn']) && !empty($post_content['testdetails'])) {
// Create equipment when not exist +++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($equipmentCreate == 1 && $total_equipment == 0){
- $sql = 'INSERT INTO equipment (productrowid,created,createdby,status,accounthierarchy,serialnumber,service_date,warranty_date) VALUES (?,?,?,?,?,?,?,?)';
+ $sql = 'INSERT INTO equipment (productrowid,created,createdby,status,accounthierarchy,serialnumber,service_date,warranty_date,order_send_date) VALUES (?,?,?,?,?,?,?,?,?)';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$productrowid,$date,$user,$status0,$account,$serial,$current_date,$current_date]);
+ $stmt->execute([$productrowid,$date,$user,$status0,$account,$serial,$service_date,$warranty_date,$order_send_date]);
$rowID = $pdo->lastInsertId();
}
@@ -311,7 +313,7 @@ if (!empty($post_content['sn']) && !empty($post_content['testdetails'])) {
//Update Equipment record
$sql = "UPDATE equipment SET service_date = ? $whereclause";
$stmt = $pdo->prepare($sql);
- $stmt->execute([$current_date]);
+ $stmt->execute([$service_date]);
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
diff --git a/api/v1/.DS_Store b/api/v1/.DS_Store
index cb2f10e..ad7d9dd 100644
Binary files a/api/v1/.DS_Store and b/api/v1/.DS_Store differ
diff --git a/api/v1/get/contracts.php b/api/v1/get/contracts.php
index 4591407..9e04cd6 100644
--- a/api/v1/get/contracts.php
+++ b/api/v1/get/contracts.php
@@ -17,11 +17,13 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el
//default whereclause
$whereclause = '';
-switch ($permission) {
- case '4':
+$hierarchy_level = getHierarchyLevel($partner);
+
+switch ($hierarchy_level) {
+ case '0':
$whereclause = '';
break;
- case '3':
+ case '1':
$condition = '__salesid___'.$partner->salesid.'___soldto___%';
$whereclause = 'WHERE c.accounthierarchy like :condition AND u.view IN (4,5)';
break;
@@ -29,7 +31,11 @@ switch ($permission) {
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
$whereclause = 'WHERE c.accounthierarchy like :condition AND u.view IN (1,2,3)';
break;
- default:
+ case '3':
+ $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%';
+ $whereclause = 'WHERE c.accounthierarchy like :condition AND u.view IN (1,2,3)';
+ break;
+ case '4':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.substr($partner->location, 0, strpos($partner->location, "-")).'%';
$whereclause = 'WHERE c.accounthierarchy like :condition AND u.view IN (1,2,3)';
break;
diff --git a/api/v1/get/history.php b/api/v1/get/history.php
index c1667e0..b844c71 100644
--- a/api/v1/get/history.php
+++ b/api/v1/get/history.php
@@ -14,11 +14,13 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el
//default whereclause
$whereclause = '';
-switch ($permission) {
- case '4':
+$hierarchy_level = getHierarchyLevel($partner);
+
+switch ($hierarchy_level) {
+ case '0':
$whereclause = '';
break;
- case '3':
+ case '1':
$condition = '__salesid___'.$partner->salesid.'___soldto___%';
$whereclause = 'WHERE e.accounthierarchy like :condition ';
break;
@@ -26,11 +28,16 @@ switch ($permission) {
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
$whereclause = 'WHERE e.accounthierarchy like :condition AND (type = "'.$type1.'" or type = "'.$type2.'" or type = "'.$type3.'" or type = "'.$type9.'" or type = "'.$type14.'" or type = "'.$type16.'")';
break;
- default:
+ case '3':
+ $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.$soldto_search;
+ $whereclause = 'WHERE e.accounthierarchy like :condition AND (type = "'.$type1.'" or type = "'.$type2.'" or type = "'.$type3.'" or type = "'.$type14.'" or type = "'.$type16.'")';
+ break;
+ case '4':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.substr($partner->location, 0, strpos($partner->location, "-")).'%';
$whereclause = 'WHERE e.accounthierarchy like :condition AND (type = "'.$type1.'" or type = "'.$type2.'" or type = "'.$type3.'" or type = "'.$type14.'" or type = "'.$type16.'")';
break;
}
+
//NEW ARRAY
$criterias = [];
$clause = '';
diff --git a/api/v1/get/user_credentials.php b/api/v1/get/user_credentials.php
index 3f98774..f7fa705 100644
--- a/api/v1/get/user_credentials.php
+++ b/api/v1/get/user_credentials.php
@@ -17,7 +17,7 @@ if ($stmt->rowCount() == 1) {
//Define User data
$partnerhierarchy = $user_data['partnerhierarchy'];
$permission = userRights($user_data['view']);
- $profile= getProfile($user_data['settings'],$permission);
+ $profile= getUserPermissions($pdo, $user_data['id']);
$username = $user_data['username'];
$useremail = $user_data['email'];
$servicekey = $user_data['service'];
diff --git a/api/v1/post/accounts.php b/api/v1/post/accounts.php
index 072c4bf..26049a7 100644
--- a/api/v1/post/accounts.php
+++ b/api/v1/post/accounts.php
@@ -40,7 +40,7 @@ if ($id != ''){
$salesid_new = (($post_content['salesid'] != '' && $post_content['salesid'] != $accounthierarchy_old->salesid)? $post_content['salesid'] : $accounthierarchy_old->salesid);
$soldto_new = (($post_content['soldto'] != '' && $post_content['soldto'] != $accounthierarchy_old->soldto)? $post_content['soldto'] : $accounthierarchy_old->soldto);
- if ($permission == 3 || $permission == 4){
+ if (getHierarchyLevel($partner) == 1 || getHierarchyLevel($partner) == 0){
//ADMIN ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
diff --git a/api/v1/post/application.php b/api/v1/post/application.php
index dd567bf..afd1e3d 100644
--- a/api/v1/post/application.php
+++ b/api/v1/post/application.php
@@ -15,7 +15,6 @@ if ($action !=''){
//Connect to DB
//------------------------------------------
$pdo = dbConnect($dbname);
-$pdo2 = dbConnect($dbname);
//------------------------------------------
//CONTENT FROM API (POST)
@@ -28,11 +27,13 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el
//default whereclause
$whereclause = 'WHERE';
-switch ($permission) {
- case '4':
+$hierarchy_level = getHierarchyLevel($partner);
+
+switch ($hierarchy_level) {
+ case '0':
$whereclause .= '';
break;
- case '3':
+ case '1':
$condition = '__salesid___'.$partner->salesid.'___soldto___%';
$whereclause = ' e.accounthierarchy like "'.$condition.'" AND ';
break;
@@ -40,7 +41,11 @@ switch ($permission) {
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
$whereclause .= ' e.accounthierarchy like "'.$condition.'" AND ';
break;
- default:
+ case '3':
+ $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%';
+ $whereclause .= ' e.accounthierarchy like "'.$condition.'" AND ';
+ break;
+ case '4':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.substr($partner->location, 0, strpos($partner->location, "-")).'%';
$whereclause .= ' e.accounthierarchy like "'.$condition.'" AND ';
break;
@@ -85,238 +90,148 @@ switch ($action) {
$communication_check = 0; //Check communication record
$message_box = [];
$timestamp = date("Y-m-d H:i:s");
-
- // Create history description
- $history_description = [
- "start_date"=>$timestamp,
- "end_date"=>date("Y-m-d", strtotime("+730 days")),
- "organization"=>strip_tags(trim($post_content['organization'])),
- "phone"=>strip_tags(trim($post_content['phone'])),
- "city"=>strip_tags(trim($post_content['city'])),
- "country"=>strip_tags(trim($post_content['country'])),
- "email_consent"=>strip_tags(trim($post_content['email_consent'])),
- "terms_consent"=>strip_tags(trim($post_content['terms_consent']))
- ];
-
- $description = json_encode($history_description, JSON_UNESCAPED_UNICODE);
+
// --------------------------------------------
// Check if multiple serialnumbers are provided
// --------------------------------------------
- if(is_array($post_content['sn'])){
- foreach ($post_content['sn'] as $sn){
- //Get equipmentid based on rowID
- $rowID = getrowID($dbname,'rowID','equipment','serialnumber="'.$sn.'"');
- if ($rowID){
- //check if under warranty
- $warranty = getrowID($dbname,'rowID','equipment_history','equipmentid="'.$rowID['rowID'].'" && (type="'.$type9.'" || type="'.$type10.'" || type="'.$type11.'" || type="'.$type12.'")');
- if ($warranty){
- // --------------------------------------------
- // Already under contract
- // --------------------------------------------
- //Serialnumber under warranty
- $message_box[] = $sn.' - '.$register_message_2;
- $communication_check = 1;
- } else
- {
- // --------------------------------------------
- // Not under warranty
- // --------------------------------------------
- //Send user firmware account
- $firmware_account_send = 1;
- //create history
- // Prepare queries
- $sql = 'INSERT INTO equipment_history (equipmentid, type, description, created, createdby,updatedby) VALUES (?,?,?,?,?,?)';
- $stmt = $pdo->prepare($sql);
- $stmt->execute([$rowID['rowID'],$type9,$description,$timestamp,$post_content['email'],$post_content['email']]);
+ // Normalize input to always be an array
+ $serial_numbers = is_array($post_content['sn']) ? $post_content['sn'] : [$post_content['sn']];
- //GET PARTNER DETAILS OF EQUIPMENT
- $partner_equipment = getrowID($dbname,'accounthierarchy','equipment','rowID="'.$rowID['rowID'].'"');
- $partner_equipment = json_decode($partner_equipment['accounthierarchy']);
-
- //Setup partnerhierarchy (salesID)
- $partnerhierarchy =[
- "salesid"=>$partner_equipment->salesid,
- "soldto"=>$partner_equipment->soldto
- ];
-
- //Setup variables for partner
- $partnername = $post_content['organization'];
- $partnernotes = 'created based on user registration';
- $salesID = json_encode($partnerhierarchy, JSON_UNESCAPED_UNICODE);
- $createdby = 'system';
-
- //Check if shipto is empty and if empty search partner or create
- if ($partner_equipment->shipto == ''){
- $partner_shipto = getrowID($dbname,'partnerID','partner','partnername = "'.$partnername.'" && partnertype="'.$partnertype3.'"');
- if ($partner_shipto){
- //Partner exists - Use it
- $partnerhierarchy['shipto'] = $partner_shipto['partnerID'].'-'.$partnername;
- } else {
- //Partner does not exist create
- $sql = 'INSERT INTO partner (partnertype,partnername,salesID,createdby,status) VALUES (?,?,?,?,?)';
- $stmt = $pdo2->prepare($sql);
- $stmt->execute([$partnertype3,$partnername,$salesID,$createdby,'1']);
-
- //Get rowID of created partner and use it
- $partner_rowid = $pdo2->lastInsertId();
- $partnerhierarchy['shipto'] = $partner_rowid.'-'.$partnername;
- }
- } else {
- // Shipto exist use it
- $partnerhierarchy['shipto'] = $partner_equipment->shipto;
- }
- //Check if location is empty and if empty search partner or create
- if ($partner_equipment->location == ''){
- $partner_location = getrowID($dbname,'partnerID','partner','partnername = "'.$partnername.'" && partnertype="'.$partnertype4.'"');
- if ($partner_location){
- //Partner exists - Use it
- $partnerhierarchy['location'] = $partner_location['partnerID'].'-'.$partnername;
-
- } else {
- //Partner does not exist create
- $sql = 'INSERT INTO partner (partnertype,partnername,salesID,createdby,status) VALUES (?,?,?,?,?)';
- $stmt = $pdo2->prepare($sql);
- $stmt->execute([$partnertype4,$partnername,$salesID,$createdby,'1']);
-
- //Get rowID of created partner and use it
- $partner_rowid = $pdo2->lastInsertId();
- $partnerhierarchy['location'] = $partner_rowid.'-'.$partnername;
- }
-
- } else {
- // Location exist use it
- $partnerhierarchy['location'] = $partner_equipment->location;
- }
-
- $shipto = $partnerhierarchy['shipto'] ?? '';
- $partnerhierarchy = json_encode($partnerhierarchy, JSON_UNESCAPED_UNICODE);
- // --------------------------------------------
- // Update equipment record warranty_date, partnerhierarchy, status equipment
- // --------------------------------------------
- $sql = 'UPDATE equipment SET status = ?, warranty_date = ?, accounthierarchy = ?,updatedby = ? WHERE rowID = ?';
- $stmt = $pdo->prepare($sql);
- $stmt->execute(['4',$warranty_extended,$partnerhierarchy,$username,$rowID['rowID']]);
-
- //Add warranty to changelog
- $warranty_user = $post_content['email'] ?? 'system';
- changelog($dbname,'equipment',$rowID['rowID'],'Warranty',$warranty_extended,$warranty_user);
-
- //Serialnumber recognized
- $message_box[] = $sn.' - '.$register_message_3;
- $communication_check = 1;
- }
- } else {
- //Serialnumber not recognized
- $message_box[] = $sn.' - '.$register_message_1;
- }
+ foreach ($serial_numbers as $sn) {
+ // Get equipment ID based on serial number
+ $sql = 'SELECT rowID, warranty_date, order_send_date from equipment where serialnumber = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$sn]);
+ $rowID = $stmt->fetch();
+
+ if (!$rowID['rowID']) {
+ // Serial number not recognized
+ $message_box[] = $sn . ' - ' . $register_message_1;
+ continue;
}
- }
- else {
- // --------------------------------------------
- //Get equipmentid based on rowID
- // --------------------------------------------
- $rowID = getrowID($dbname,'rowID','equipment','serialnumber="'.$post_content['sn'].'"');
- if ($rowID){
- //check if under warranty
- $warranty = getrowID($dbname,'rowID','equipment_history','equipmentid="'.$rowID['rowID'].'" && (type="'.$type9.'" || type="'.$type10.'" || type="'.$type11.'" || type="'.$type12.'")');
- if ($warranty){
- // --------------------------------------------
+
+ // Check if under warranty
+ $warranty_types = [$type9, $type10, $type11, $type12];
+ $warranty_condition = 'equipmentid="' . $rowID['rowID'] . '" && (type="' . implode('" || type="', $warranty_types) . '")';
+ $warranty = getrowID($dbname, 'rowID', 'equipment_history', $warranty_condition);
+
+ if ($warranty) {
// Already under contract
- // --------------------------------------------
- //Serialnumber not recognized
- $message_box[] = $post_content['sn'].' - '.$register_message_2;
- } else
- {
- // --------------------------------------------
- // Not under warranty
- // --------------------------------------------
- $firmware_account_send = 1;
- //create history
- $sql = 'INSERT INTO equipment_history (equipmentid, type, description, created, createdby, updatedby) VALUES (?,?,?,?,?,?)';
- $stmt = $pdo->prepare($sql);
- $stmt->execute([$rowID['rowID'],$type9,$description,$timestamp,$post_content['email'],$post_content['email']]);
-
- //GET PARTNER DETAILS OF EQUIPMENT
- $partner_equipment = getrowID($dbname,'accounthierarchy','equipment','rowID="'.$rowID['rowID'].'"');
- $partner_equipment = json_decode($partner_equipment['accounthierarchy']);
-
- //Setup partnerhierarchy (salesID)
- $partnerhierarchy =[
- "salesid"=>$partner_equipment->salesid,
- "soldto"=>$partner_equipment->soldto
- ];
-
- //Setup variables for partner
- $partnername = $post_content['organization'];
- $partnernotes = 'created based on user registration';
- $salesID = json_encode($partnerhierarchy, JSON_UNESCAPED_UNICODE);
- $createdby = 'system';
-
- //Check if shipto is empty and if empty search partner or create
- if ($partner_equipment->shipto == ''){
- $partner_shipto = getrowID($dbname,'partnerID','partner','partnername = "'.$partnername.'" && partnertype="'.$partnertype3.'"');
- if ($partner_shipto){
- //Partner exists - Use it
- $partnerhierarchy['shipto'] = $partner_shipto['partnerID'].'-'.$partnername;
- } else {
- //Partner does not exist create
- $sql = 'INSERT INTO partner (partnertype, partnername,salesID,createdby,status) VALUES (?,?,?,?,?)';
- $stmt = $pdo2->prepare($sql);
- $stmt->execute([$partnertype3,$partnername,$salesID,$createdby,'1']);
-
- //Get rowID of created partner and use it
- $partner_rowid = $pdo2->lastInsertId();
- $partnerhierarchy['shipto'] = $partner_rowid.'-'.$partnername;
- }
- } else {
- // Shipto exist use it
- $partnerhierarchy['shipto'] = $partner_equipment->shipto;
- }
- //Check if location is empty and if empty search partner or create
- if ($partner_equipment->location == ''){
- $partner_location = getrowID($dbname,'partnerID','partner','partnername = "'.$partnername.'" && partnertype="'.$partnertype4.'"');
- if ($partner_location){
- //Partner exists - Use it
- $partnerhierarchy['location'] = $partner_location['partnerID'].'-'.$partnername;
-
- } else {
- //Partner does not exist create
- $sql = 'INSERT INTO partner (partnertype,partnername,salesID,createdby,status) VALUES (?,?,?,?,?)';
- $stmt = $pdo2->prepare($sql);
- $stmt->execute([$partnertype4,$partnername,$salesID,$createdby,'1']);
-
- //Get rowID of created partner and use it
- $partner_rowid = $pdo2->lastInsertId();
- $partnerhierarchy['location'] = $partner_rowid.'-'.$partnername;
- }
- } else {
- // Location exist use it
- $partnerhierarchy['location'] = $partner_equipment->location;
- }
-
- $partnerhierarchy = json_encode($partnerhierarchy, JSON_UNESCAPED_UNICODE);
-
- // --------------------------------------------
- // Update equipment record warranty_date, partnerhierarchy, status equipment
- // --------------------------------------------
- $sql = 'UPDATE equipment SET status = ?, warranty_date = ?, accounthierarchy = ?, updatedby = ? WHERE rowID = ?';
- $stmt = $pdo->prepare($sql);
- $stmt->execute(['4',$warranty_extended,$partnerhierarchy,$username,$rowID['rowID']]);
-
- //Add warranty to changelog
- $warranty_user = $post_content['email'] ?? 'system';
- changelog($dbname,'equipment',$rowID['rowID'],'Warranty',$warranty_extended,$warranty_user);
-
- //Serialnumber recognized
- $message_box[] = $post_content['sn'].' - '.$register_message_3;
- }
+ $message_box[] = $sn . ' - ' . $register_message_2;
+ $communication_check = 1;
+ continue;
}
- else {
- //Serialnumber not recognized
- $message_box[] = $post_content['sn'].' - '.$register_message_1;
+
+ //define warranty_end_date
+ $order_send_date = $rowID['order_send_date'] ?? $rowID['warranty_date'];
+
+ // Check if order_send_date is available
+ if (empty($order_send_date)) {
+ // No valid date found - skip this serial number
+ $message_box[] = $sn . ' - ' . $register_message_1; // or create a specific message for missing date
+ continue;
}
- }
+
+ // Calculate warranty end date based on eligibility window
+ $current_date = new DateTime();
+ $order_date = new DateTime($order_send_date);
+ $months_diff = $current_date->diff($order_date)->m + ($current_date->diff($order_date)->y * 12);
+
+ if ($months_diff <= WARRANTY_ELIGIBILITY_WINDOW) {
+ // Within eligibility window - apply extended warranty
+ $warranty_end_date = (clone $order_date)->modify('+' . WARRANTY_EXTENDED_MONTH . ' months')->format('Y-m-d');
+ } else {
+ // Outside eligibility window - apply standard warranty
+ $warranty_end_date = (clone $order_date)->modify('+' . WARRANTY_MONTHS . ' months')->format('Y-m-d');
+ }
+
+ // Not under warranty - process registration
+ $firmware_account_send = 1;
+
+ //Create history description
+ $history_description = [
+ "start_date"=>$timestamp,
+ "end_date"=> $warranty_end_date,
+ "organization"=>strip_tags(trim($post_content['organization'])),
+ "phone"=>strip_tags(trim($post_content['phone'])),
+ "city"=>strip_tags(trim($post_content['city'])),
+ "country"=>strip_tags(trim($post_content['country'])),
+ "email_consent"=>strip_tags(trim($post_content['email_consent'])),
+ "terms_consent"=>strip_tags(trim($post_content['terms_consent']))
+ ];
+
+ $description = json_encode($history_description, JSON_UNESCAPED_UNICODE);
+
+ // Create history entry
+ $sql = 'INSERT INTO equipment_history (equipmentid, type, description, created, createdby, updatedby) VALUES (?,?,?,?,?,?)';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([
+ $rowID['rowID'],
+ $type9,
+ $description,
+ $timestamp,
+ $post_content['email'],
+ $post_content['email']
+ ]);
+
+ // Get partner details of equipment
+ $partner_equipment = getrowID($dbname, 'accounthierarchy', 'equipment', 'rowID="' . $rowID['rowID'] . '"');
+ $partner_equipment = json_decode($partner_equipment['accounthierarchy']);
+
+ // Setup partner hierarchy
+ $partnerhierarchy = [
+ "salesid" => $partner_equipment->salesid,
+ "soldto" => $partner_equipment->soldto
+ ];
+
+ // Setup variables for partner
+ $partnername = $post_content['organization'];
+ $salesID = json_encode($partnerhierarchy, JSON_UNESCAPED_UNICODE);
+ $createdby = 'system';
+
+ // Helper function to get or create partner
+ $getOrCreatePartner = function($partnertype) use ($dbname, $partnername, $salesID, $createdby, $pdo) {
+ $partner = getrowID($dbname, 'partnerID', 'partner', 'partnername = "' . $partnername . '" && partnertype="' . $partnertype . '"');
+
+ if ($partner) {
+ return $partner['partnerID'] . '-' . $partnername;
+ }
+
+ // Partner does not exist - create
+ $sql = 'INSERT INTO partner (partnertype, partnername, salesID, createdby, status) VALUES (?,?,?,?,?)';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$partnertype, $partnername, $salesID, $createdby, '1']);
+
+ $partner_rowid = $pdo->lastInsertId();
+ return $partner_rowid . '-' . $partnername;
+ };
+
+ // Handle shipto
+ $partnerhierarchy['shipto'] = empty($partner_equipment->shipto)
+ ? $getOrCreatePartner($partnertype3)
+ : $partner_equipment->shipto;
+
+ // Handle location
+ $partnerhierarchy['location'] = empty($partner_equipment->location)
+ ? $getOrCreatePartner($partnertype4)
+ : $partner_equipment->location;
+
+ $partnerhierarchy_json = json_encode($partnerhierarchy, JSON_UNESCAPED_UNICODE);
+
+ // Update equipment record
+ $sql = 'UPDATE equipment SET status = ?, warranty_date = ?, accounthierarchy = ?, updatedby = ? WHERE rowID = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute(['4', $warranty_end_date, $partnerhierarchy_json, $username, $rowID['rowID']]);
+
+ // Add warranty to changelog
+ $warranty_user = $post_content['email'] ?? 'system';
+ changelog($dbname, 'equipment', $rowID['rowID'], 'Warranty', $warranty_end_date, $warranty_user);
+
+ // Serial number recognized
+ $message_box[] = $sn . ' - ' . $register_message_3;
+ $communication_check = 1;
+ }
// --------------------------------------------
// Send generic account to user for software updates
diff --git a/api/v1/post/contracts.php b/api/v1/post/contracts.php
index 498f164..12f90e7 100644
--- a/api/v1/post/contracts.php
+++ b/api/v1/post/contracts.php
@@ -58,7 +58,7 @@ if ($id != ''){
$shipto_new = (($post_content['shipto'] != '' && $post_content['shipto'] != $contract_old->shipto)? $post_content['shipto'] : $contract_old->shipto);
$location_new = (($post_content['location'] != '' && $post_content['location'] != $contract_old->location)? $post_content['location'] : $contract_old->location);
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -67,7 +67,7 @@ if ($id != ''){
"location"=>$location_new
);
}
- elseif ($permission == 3) {
+ elseif (getHierarchyLevel($partner) == 1) {
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$contract_old->salesid,
@@ -120,7 +120,7 @@ if ($id != ''){
}
else {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
$account = array(
"salesid"=>$post_content['salesid'],
"soldto"=>$post_content['soldto'],
@@ -128,7 +128,7 @@ else {
"location"=>$post_content['location']
);
}
- elseif ($permission == 3){
+ elseif (getHierarchyLevel($partner) == 1){
$account = array(
"salesid"=>$partner->salesid,
"soldto"=>$post_content['soldto'],
@@ -160,7 +160,7 @@ if (isset($post_content['servicetool'])){
if (isset($post_content['ignore_list'])){
$post_content['ignore_list'] = json_encode($post_content['ignore_list'], JSON_UNESCAPED_UNICODE);
//ONLY ADMINS ARE ALLOWED TO UPDATE IGNORE LIST
- if ($permission != 3 && $permission != 4){
+ if (getHierarchyLevel($partner) != 1 && getHierarchyLevel($partner) != 0){
unset($post_content['ignore_list']);
}
}
diff --git a/api/v1/post/equipments.php b/api/v1/post/equipments.php
index 7200512..198012c 100644
--- a/api/v1/post/equipments.php
+++ b/api/v1/post/equipments.php
@@ -47,7 +47,7 @@ if ($id != ''){
$owner_equipment = (($equipment_data['createdby'] == $username)? 1 : 0);
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -57,7 +57,7 @@ if ($id != ''){
"section"=>$section_new
);
}
- elseif ($permission == 3) {
+ elseif (getHierarchyLevel($partner) == 1) {
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$equipment_old->salesid,
@@ -79,7 +79,7 @@ if ($id != ''){
}
else {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
$account = array(
"salesid"=>$post_content['salesid'],
"soldto"=>$post_content['soldto'],
@@ -89,7 +89,7 @@ else {
);
}
- elseif ($permission == 3){
+ elseif (getHierarchyLevel($partner) == 1){
$account = array(
"salesid"=>$partner->salesid,
"soldto"=>$post_content['soldto'],
diff --git a/api/v1/post/partners.php b/api/v1/post/partners.php
index 6f1be77..ea07e22 100644
--- a/api/v1/post/partners.php
+++ b/api/v1/post/partners.php
@@ -41,7 +41,7 @@ if ($id != ''){
$salesid_new = (($post_content['salesid'] != '' && $post_content['salesid'] != $partnerhierarchy_old->salesid)? $post_content['salesid'] : $partnerhierarchy_old->salesid);
$soldto_new = (($post_content['soldto'] != '' && $post_content['soldto'] != $partnerhierarchy_old->soldto)? $post_content['soldto'] : $partnerhierarchy_old->soldto);
- if ($permission == 3 || $permission == 4){
+ if (getHierarchyLevel($partner) == 1 || getHierarchyLevel($partner) == 0){
//ADMIN ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -56,7 +56,7 @@ if ($id != ''){
}
else {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 3 || $permission == 4){
+ if (getHierarchyLevel($partner) == 1 || getHierarchyLevel($partner) == 0){
//ADMIN ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$partner->salesid,
diff --git a/api/v1/post/users.php b/api/v1/post/users.php
index 1d35037..96ca3b1 100644
--- a/api/v1/post/users.php
+++ b/api/v1/post/users.php
@@ -50,7 +50,7 @@ $soldto_new = ((isset($post_content['soldto']) && $post_content['soldto'] != ''
$shipto_new = (($post_content['shipto'] != '' && $post_content['shipto'] != $partnerhierarchy_old->shipto)? $post_content['shipto'] : $partnerhierarchy_old->shipto);
$location_new = (($post_content['location'] != '' && $post_content['location'] != $partnerhierarchy_old->location)? $post_content['location'] : $partnerhierarchy_old->location);
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -58,7 +58,7 @@ $location_new = (($post_content['location'] != '' && $post_content['location'] !
"shipto"=>$shipto_new,
"location"=>$location_new
);
- }elseif ($permission == 3) {
+ }elseif (getHierarchyLevel($partner) == 1) {
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$partner->salesid,
@@ -77,7 +77,7 @@ $location_new = (($post_content['location'] != '' && $post_content['location'] !
}
} elseif ($command == 'insert') {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$post_content['salesid'],
@@ -86,7 +86,7 @@ $location_new = (($post_content['location'] != '' && $post_content['location'] !
"location"=>$post_content['location']
);
}
- elseif ($permission == 3){
+ elseif (getHierarchyLevel($partner) == 1){
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$partner->salesid,
diff --git a/api/v2/.DS_Store b/api/v2/.DS_Store
index c28ab40..16d8e39 100644
Binary files a/api/v2/.DS_Store and b/api/v2/.DS_Store differ
diff --git a/api/v2/get/.DS_Store b/api/v2/get/.DS_Store
deleted file mode 100644
index 5008ddf..0000000
Binary files a/api/v2/get/.DS_Store and /dev/null differ
diff --git a/api/v2/get/access_elements.php b/api/v2/get/access_elements.php
new file mode 100644
index 0000000..b505979
--- /dev/null
+++ b/api/v2/get/access_elements.php
@@ -0,0 +1,158 @@
+prepare($sql);
+
+//------------------------------------------
+//Bind to query
+//------------------------------------------
+if (!empty($criterias)){
+ foreach ($criterias as $key => $value){
+ $key_condition = ':'.$key;
+ if (str_contains($sql, $key_condition)){
+ if ($key == 'search'){
+ $search_value = '%'.$value.'%';
+ $stmt->bindValue($key, $search_value, PDO::PARAM_STR);
+ }
+ elseif ($key == 'p'){
+ //Do nothing (bug)
+ }
+ else {
+ $stmt->bindValue($key, $value, PDO::PARAM_STR);
+ }
+ }
+ }
+}
+
+//------------------------------------------
+// Debuglog
+//------------------------------------------
+if (debug){
+ $message = $date.';'.$sql.';'.$username;
+ debuglog($message);
+}
+
+//------------------------------------------
+//Add paging details
+//------------------------------------------
+$page_rows = $page_rows_equipment ?? 20;
+
+if(isset($criterias['totals']) && $criterias['totals']==''){
+ $stmt->execute();
+ $messages = $stmt->fetch();
+ $messages = $messages[0];
+}
+elseif(isset($criterias['all']) && $criterias['all']==''){
+ //Return all records (no paging)
+ $stmt->execute();
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+else {
+ $current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1;
+ $stmt->bindValue('page', ($current_page - 1) * $page_rows, PDO::PARAM_INT);
+ $stmt->bindValue('num_rows', $page_rows, PDO::PARAM_INT);
+ //Execute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+//------------------------------------------
+//JSON_EnCODE
+//------------------------------------------
+$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
+//------------------------------------------
+//Send results
+//------------------------------------------
+echo $messages;
+
+?>
diff --git a/api/v2/get/contracts.php b/api/v2/get/contracts.php
index 2362582..f5da5bc 100644
--- a/api/v2/get/contracts.php
+++ b/api/v2/get/contracts.php
@@ -17,19 +17,25 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el
//default whereclause
$whereclause = '';
-switch ($permission) {
- case '4':
+$hierarchy_level = getHierarchyLevel($partner);
+
+switch ($hierarchy_level) {
+ case '0':
$whereclause = '';
break;
- case '3':
+ case '1':
$condition = '__salesid___'.$partner->salesid.'___soldto___%';
$whereclause = 'WHERE accounthierarchy like :condition AND u.view IN (4,5)';
break;
case '2':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
$whereclause = 'WHERE accounthierarchy like :condition AND u.view IN (1,2,3)';
+ break;
+ case '3':
+ $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%';
+ $whereclause = 'WHERE accounthierarchy like :condition AND u.view IN (1,2,3)';
break;
- default:
+ case '4':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.substr($partner->location, 0, strpos($partner->location, "-")).'%';
$whereclause = 'WHERE accounthierarchy like :condition AND u.view IN (1,2,3)';
break;
diff --git a/api/v2/get/equipment_history.php b/api/v2/get/equipment_history.php
new file mode 100644
index 0000000..1fc241e
--- /dev/null
+++ b/api/v2/get/equipment_history.php
@@ -0,0 +1,116 @@
+ isset($_GET['serialnumber']) ? trim($_GET['serialnumber']) : null,
+ 'type' => isset($_GET['type']) ? trim($_GET['type']) : null,
+ 'start' => isset($_GET['start']) ? trim($_GET['start']) : date("Y-m-d", strtotime("-270 days")),
+ 'end' => isset($_GET['end']) ? trim($_GET['end']) : date("Y-m-d", strtotime("+1 days"))
+];
+
+// ============================================
+// Build Query with Prepared Statements
+// ============================================
+
+$whereClauses = [];
+$params = [];
+
+// Serial Number Filter
+if ($filters['serialnumber']) {
+ $whereClauses[] = 'h.description LIKE :serialnumber';
+ $params[':serialnumber'] = "%historycreated%SN%:" . $filters['serialnumber'] . "%";
+ $whereClauses[] = 'h.type != :excluded_type';
+ $params[':excluded_type'] = 'SRIncluded';
+}
+
+// Type Filter
+if ($filters['type']) {
+ if ($filters['type'] === 'latest') {
+ // Get only the latest record per equipment
+ if ($filters['serialnumber']) {
+ $whereClauses[] = 'h.rowID IN (
+ SELECT MAX(h2.rowID)
+ FROM equipment_history h2
+ GROUP BY h2.equipmentid
+ )';
+ } else {
+ $whereClauses[] = "h.description LIKE '%historycreated%'";
+ $whereClauses[] = 'h.rowID IN (
+ SELECT MAX(h2.rowID)
+ FROM equipment_history h2
+ WHERE h2.description LIKE :history_created
+ GROUP BY h2.equipmentid
+ )';
+ $params[':history_created'] = '%historycreated%';
+ }
+ } else {
+ // Specific type filter
+ $whereClauses[] = 'h.type = :type';
+ $params[':type'] = $filters['type'];
+ }
+}
+
+// Default filter if no other filters applied
+if (empty($whereClauses)) {
+ $whereClauses[] = "h.description LIKE '%historycreated%'";
+}
+
+// Date Range Filter
+$whereClauses[] = 'h.created BETWEEN :start_date AND :end_date';
+$params[':start_date'] = $filters['start'];
+$params[':end_date'] = $filters['end'];
+
+// ============================================
+// Execute Query
+// ============================================
+
+$whereClause = 'WHERE ' . implode(' AND ', $whereClauses);
+$sql = "SELECT h.rowID, h.description
+ FROM equipment_history h
+ $whereClause
+ ORDER BY h.created DESC";
+
+try {
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($params);
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ // ============================================
+ // Format Response
+ // ============================================
+
+ $results = [];
+ foreach ($messages as $message) {
+ $record = json_decode($message['description'], true);
+
+ // Handle JSON decode errors
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ continue; // Skip invalid JSON
+ }
+
+ $record['historyID'] = (int)$message['rowID'];
+ $results[] = $record;
+ }
+
+ // Set proper headers
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+
+} catch (PDOException $e) {
+ // Log error (don't expose to client in production)
+ error_log("Database error: " . $e->getMessage());
+
+ //header('Content-Type: application/json; charset=utf-8', true, 500);
+ echo json_encode([
+ 'error' => 'An error occurred while processing your request'
+ ]);
+}
+
+?>
\ No newline at end of file
diff --git a/api/v2/get/equipments.php b/api/v2/get/equipments.php
index d02c76d..ab70944 100644
--- a/api/v2/get/equipments.php
+++ b/api/v2/get/equipments.php
@@ -143,6 +143,10 @@ if(isset($get_content) && $get_content!=''){
$clause .= ' AND e.serialnumber IN (:'.$v[0].')';
}
}
+ elseif ($v[0] == 'validate') {
+ // Set validation mode flag
+ $validation_mode = true;
+ }
elseif ($v[0] == 'firmware') {
//Assets with firmaware upgrade = 0 (1=latest version, 2=No software)
$clause .= ' AND e.status != 5 AND e.sw_version_latest = 0';
@@ -161,7 +165,7 @@ if(isset($get_content) && $get_content!=''){
}
}
-if ($sw_version_latest_update == 1){
+if ($sw_version_latest_update == 1 || $clause == ''){
//------------------------------------------
//UPDATE SW_STATUS
//------------------------------------------
@@ -175,6 +179,10 @@ if (isset($criterias['download']) && $criterias['download'] ==''){
//Request for download
$sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname from equipment e LEFT JOIN products p ON e.productrowid = p.rowID '.$whereclause.' ORDER BY equipmentID';
}
+elseif (isset($validation_mode) && $validation_mode === true) {
+ // Validation mode - return count only for serial validation
+ $sql = "SELECT count(rowID) as rowID from equipment e $whereclause";
+}
elseif (isset($criterias['totals']) && $criterias['totals'] =='' && !isset($criterias['type'])){
//Request for total rows
$sql = 'SELECT count(*) as count from equipment e LEFT JOIN products p ON e.productrowid = p.rowID '.$whereclause.'';
@@ -267,7 +275,7 @@ else {
}
//SQL for Paging
- $sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media from equipment e LEFT JOIN products p ON e.productrowid = p.rowID '.$whereclause.' ORDER BY '.$sort.' LIMIT :page,:num_products';
+ $sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media, psl.starts_at,psl.expires_at,psl.status as license_status from equipment e LEFT JOIN products p ON e.productrowid = p.rowID LEFT JOIN products_software_licenses psl ON e.sw_version_license = psl.license_key '.$whereclause.' ORDER BY '.$sort.' LIMIT :page,:num_products';
}
$stmt = $pdo->prepare($sql);
@@ -314,7 +322,19 @@ if (debug){
//------------------------------------------
//Add paging details
//------------------------------------------
-if(isset($criterias['totals']) && $criterias['totals']==''){
+if (isset($validation_mode) && $validation_mode === true) {
+ $stmt->execute();
+ $messages = $stmt->fetch();
+
+ if ($messages[0] == 1) {
+ echo json_encode(array('SN'=> TRUE));
+ }
+ else {
+ echo json_encode(array('SN'=> FALSE));
+ }
+ return;
+}
+elseif(isset($criterias['totals']) && $criterias['totals']==''){
$stmt->execute();
$messages = $stmt->fetch();
$messages = $messages[0];
diff --git a/api/v2/get/history.php b/api/v2/get/history.php
index be5c826..aa81a11 100644
--- a/api/v2/get/history.php
+++ b/api/v2/get/history.php
@@ -13,27 +13,34 @@ if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} el
//default whereclause
$whereclause = '';
+$hierarchy_level = getHierarchyLevel($partner);
-switch ($permission) {
- case '4':
+switch ($hierarchy_level) {
+ case '0':
$whereclause = '';
break;
- case '3':
+ case '1':
$condition = '__salesid___'.$partner->salesid.'___soldto___%';
- $whereclause = 'WHERE e.accounthierarchy like :condition';
+ $whereclause = 'WHERE e.accounthierarchy like :condition ';
break;
case '2':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
$whereclause = 'WHERE e.accounthierarchy like :condition AND (type = "'.$type1.'" or type = "'.$type2.'" or type = "'.$type3.'" or type = "'.$type9.'" or type = "'.$type14.'" or type = "'.$type16.'")';
break;
- default:
+ case '3':
+ $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.$soldto_search;
+ $whereclause = 'WHERE e.accounthierarchy like :condition AND (type = "'.$type1.'" or type = "'.$type2.'" or type = "'.$type3.'" or type = "'.$type14.'" or type = "'.$type16.'")';
+ break;
+ case '4':
$condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.substr($partner->location, 0, strpos($partner->location, "-")).'%';
$whereclause = 'WHERE e.accounthierarchy like :condition AND (type = "'.$type1.'" or type = "'.$type2.'" or type = "'.$type3.'" or type = "'.$type14.'" or type = "'.$type16.'")';
break;
}
+
//NEW ARRAY
$criterias = [];
$clause = '';
+$type_check = false;
//Check for $_GET variables and build up clause
if(isset($get_content) && $get_content!=''){
@@ -64,11 +71,65 @@ if(isset($get_content) && $get_content!=''){
//build up search
$clause .= ' AND (h.rowID like :'.$v[0].' OR h.createdby like :'.$v[0].')';
}
- elseif ($v[0] == 'type' && $v[1] == 'servicereport') {
- //Filter out only relevant servicereports
- $filter_key_1 = '"%serialnumber%"';
- $filter_key_2 = '"ServiceReport"';
- $clause .= ' AND h.type = '.$filter_key_2.' AND NOT e.productrowid = "31" AND h.description like '.$filter_key_1;
+ elseif ($v[0] == 'serialnumber') {
+ //build up serialnumber
+ //check if multiple serialnumbers are provided
+ if (str_contains($v[1], ',')){
+ $inputs = explode(",",$v[1]);
+ $new_querystring = ''; //empty querystring
+ $x=0;
+ foreach($inputs as $input){
+ //create key
+ $new_key = $v[0].'_'.$x;
+ //inject new key/value to array
+ $criterias[$new_key] = $input;
+ $new_querystring .= ':'.$new_key.',';
+ $x++;
+ }
+ //remove obsolete last character from new_querystring
+ $new_querystring = substr($new_querystring,0, -1);
+ //add new_querystring to clause
+ $clause .= ' AND e.serialnumber IN ('.$new_querystring.')';
+ //remove original key/value from array
+ unset($criterias[$v[0]]);
+ }
+ else {
+ $clause .= ' AND e.serialnumber IN (:'.$v[0].')';
+ }
+ }
+ elseif ($v[0] == 'type') {
+ if ($v[1] == 'servicereport') {
+ //Filter out only relevant servicereports
+ $filter_key_1 = '"%serialnumber%"';
+ $filter_key_2 = '"ServiceReport"';
+ $clause .= ' AND h.type = '.$filter_key_2.' AND NOT e.productrowid = "31" AND h.description like '.$filter_key_1;
+ //remove from criterias to prevent double binding
+ unset($criterias[$v[0]]);
+ }
+ elseif (str_contains($v[1], ',')) {
+ //check if multiple types are provided
+ $inputs = explode(",",$v[1]);
+ $new_querystring = ''; //empty querystring
+ $x=0;
+ foreach($inputs as $input){
+ //create key
+ $new_key = $v[0].'_'.$x;
+ //inject new key/value to array
+ $criterias[$new_key] = $input;
+ $new_querystring .= ':'.$new_key.',';
+ $x++;
+ }
+ //remove obsolete last character from new_querystring
+ $new_querystring = substr($new_querystring,0, -1);
+ //add new_querystring to clause
+ $clause .= ' AND h.type IN ('.$new_querystring.')';
+ //remove original key/value from array
+ $type_check = true;
+ unset($criterias[$v[0]]);
+ }
+ else {
+ $clause .= ' AND h.type = :'.$v[0];
+ }
}
elseif ($v[0] == 'created') {
//build up search
@@ -89,6 +150,9 @@ if(isset($criterias['totals']) && $criterias['totals'] ==''){
//Request for total rows
$sql ='SELECT count(h.rowID) as historyID FROM equipment_history h LEFT JOIN equipment e ON h.equipmentid = e.rowID '.$whereclause.'';
}
+elseif($type_check){
+ $sql ='SELECT h.rowID as historyID, e.rowID as equipmentID, e.serialnumber, h.type, h.description, h.created, h.createdby FROM equipment_history h LEFT JOIN equipment e ON h.equipmentid = e.rowID '.$whereclause.' ORDER BY h.created DESC';
+}
else {
//request history
$sql ='SELECT h.rowID as historyID, e.rowID as equipmentID, e.serialnumber, h.type, h.description, h.created, h.createdby FROM equipment_history h LEFT JOIN equipment e ON h.equipmentid = e.rowID '.$whereclause.' ORDER BY h.created DESC LIMIT :page,:num_products';
@@ -125,6 +189,12 @@ if(isset($criterias['totals']) && $criterias['totals']==''){
$messages = $stmt->fetch();
$messages = $messages[0];
}
+elseif($type_check){
+ //Excute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
else {
$current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1;
$stmt->bindValue('page', ($current_page - 1) * $page_rows_history, PDO::PARAM_INT);
@@ -136,10 +206,22 @@ else {
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
+// Clean up nested JSON in description fields before final encoding
+if (!isset($criterias['totals']) || $criterias['totals'] != '') {
+ foreach ($messages as &$message) {
+ if (isset($message['description']) && is_string($message['description'])) {
+ $decoded = json_decode($message['description'], true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ $message['description'] = json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ }
+ }
+ }
+}
+
//------------------------------------------
//JSON_ENCODE
//------------------------------------------
-$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
+$messages = json_encode($messages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
//Send results
echo $messages;
diff --git a/api/v2/get/invoice.php b/api/v2/get/invoice.php
index 8091646..07055c6 100644
--- a/api/v2/get/invoice.php
+++ b/api/v2/get/invoice.php
@@ -51,7 +51,7 @@ elseif (isset($criterias['list']) && $criterias['list'] =='invoice'){
//SQL for Paging
$sql = 'SELECT tx.*, txi.item_id as item_id,txi.item_price as item_price, txi.item_quantity as item_quantity, txi.item_options as item_options, p.productcode, p.productname, inv.id as invoice, inv.created as invoice_created, i.language as user_language
FROM transactions tx
- left join invoice inv ON tx.id = inv.txn_id
+ left join invoice inv ON tx.txn_id = inv.txn_id
left join transactions_items txi ON tx.id = txi.txn_id
left join products p ON p.rowID = txi.item_id
left join identity i ON i.userkey = tx.account_id '.$whereclause;
diff --git a/api/v2/get/marketing_files.php b/api/v2/get/marketing_files.php
new file mode 100644
index 0000000..6efa96c
--- /dev/null
+++ b/api/v2/get/marketing_files.php
@@ -0,0 +1,152 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+$whereclause = '';
+
+//NEW ARRAY
+$criterias = [];
+$clause = '';
+
+//Check for $_GET variables and build up clause
+if(isset($get_content) && $get_content!=''){
+ //GET VARIABLES FROM URL
+ $requests = explode("&", $get_content);
+ //Check for keys and values
+ foreach ($requests as $y){
+ $v = explode("=", $y);
+ //INCLUDE VARIABLES IN ARRAY
+ $criterias[$v[0]] = $v[1];
+
+ if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] == 'action' || $v[0] =='success_msg' || $v[0] == '_t'){
+ //do nothing
+ }
+ elseif ($v[0] == 'folder_id') {
+ if ($v[1] === 'null' || $v[1] === '') {
+ $clause .= ' AND folder_id IS NULL';
+ } else {
+ $clause .= ' AND folder_id = :folder_id';
+ }
+ }
+ elseif ($v[0] == 'search') {
+ $clause .= ' AND (title LIKE :search OR original_filename LIKE :search)';
+ }
+ elseif ($v[0] == 'tag') {
+ $clause .= ' AND EXISTS (SELECT 1 FROM marketing_file_tags ft JOIN marketing_tags t ON ft.tag_id = t.id WHERE ft.file_id = mf.id AND t.tag_name = :tag)';
+ }
+ elseif ($v[0] == 'file_type') {
+ $clause .= ' AND file_type = :file_type';
+ }
+ else {
+ // Ignore unknown parameters
+ }
+ }
+ if ($whereclause == '' && $clause !=''){
+ $whereclause = 'WHERE '.substr($clause, 4);
+ } else {
+ $whereclause .= $clause;
+ }
+}
+
+//Set page
+$pagina = 1;
+if(isset($criterias['p']) && $criterias['p'] !='') {
+ $pagina = $criterias['p'];
+}
+
+//Set limit
+$limit = 50;
+if(isset($criterias['limit']) && $criterias['limit'] !='') {
+ $limit = intval($criterias['limit']);
+}
+$offset = ($pagina - 1) * $limit;
+
+//check for totals call
+if(isset($criterias['totals'])){
+ $sql = 'SELECT COUNT(*) as found FROM marketing_files mf '.$whereclause.' ';
+ $stmt = $pdo->prepare($sql);
+
+ // Bind parameters
+ if (!empty($criterias)) {
+ foreach ($criterias as $key => $value) {
+ if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'limit' && $key !== 'action') {
+ if ($key == 'search') {
+ $stmt->bindValue(':'.$key, '%'.$value.'%');
+ } elseif ($key == 'folder_id' && ($value === 'null' || $value === '')) {
+ continue;
+ } else {
+ $stmt->bindValue(':'.$key, $value);
+ }
+ }
+ }
+ }
+
+ $stmt->execute();
+ $found = $stmt->fetchColumn();
+ echo $found;
+ exit;
+}
+
+// Main query
+$sql = "SELECT
+ mf.*,
+ GROUP_CONCAT(mt.tag_name) as tags
+FROM marketing_files mf
+LEFT JOIN marketing_file_tags mft ON mf.id = mft.file_id
+LEFT JOIN marketing_tags mt ON mft.tag_id = mt.id
+" . $whereclause . "
+GROUP BY mf.id
+ORDER BY mf.created DESC
+LIMIT " . $limit . " OFFSET " . $offset;
+
+$stmt = $pdo->prepare($sql);
+
+// Bind parameters
+if (!empty($criterias)) {
+ foreach ($criterias as $key => $value) {
+ if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'limit') {
+ if ($key == 'search') {
+ $stmt->bindValue(':'.$key, '%'.$value.'%');
+ } elseif ($key == 'folder_id' && ($value === 'null' || $value === '')) {
+ continue;
+ } else {
+ $stmt->bindValue(':'.$key, $value);
+ }
+ }
+ }
+}
+
+$stmt->execute();
+$marketing_files = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Process each file
+foreach ($marketing_files as &$file) {
+ // Process tags
+ $file['tags'] = $file['tags'] ? explode(',', $file['tags']) : [];
+
+ // Format file size
+ $bytes = $file['file_size'];
+ if ($bytes >= 1073741824) {
+ $file['file_size_formatted'] = number_format($bytes / 1073741824, 2) . ' GB';
+ } elseif ($bytes >= 1048576) {
+ $file['file_size_formatted'] = number_format($bytes / 1048576, 2) . ' MB';
+ } elseif ($bytes >= 1024) {
+ $file['file_size_formatted'] = number_format($bytes / 1024, 2) . ' KB';
+ } else {
+ $file['file_size_formatted'] = $bytes . ' B';
+ }
+}
+
+// Return result
+echo json_encode($marketing_files, JSON_UNESCAPED_UNICODE);
+exit;
\ No newline at end of file
diff --git a/api/v2/get/marketing_folders.php b/api/v2/get/marketing_folders.php
new file mode 100644
index 0000000..fed5660
--- /dev/null
+++ b/api/v2/get/marketing_folders.php
@@ -0,0 +1,165 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+$whereclause = '';
+
+//NEW ARRAY
+$criterias = [];
+$clause = '';
+
+//Check for $_GET variables and build up clause
+if(isset($get_content) && $get_content!=''){
+ //GET VARIABLES FROM URL
+ $requests = explode("&", $get_content);
+ //Check for keys and values
+ foreach ($requests as $y){
+ $v = explode("=", $y);
+ //INCLUDE VARIABLES IN ARRAY
+ $criterias[$v[0]] = $v[1];
+
+ if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] =='success_msg' || $v[0] == 'action' || $v[0] == 'tree'){
+ //do nothing - these are not SQL parameters
+ }
+ elseif ($v[0] == 'parent_id') {
+ if ($v[1] === 'null' || $v[1] === '') {
+ $clause .= ' AND parent_id IS NULL';
+ } else {
+ $clause .= ' AND parent_id = :parent_id';
+ }
+ }
+ elseif ($v[0] == 'search') {
+ $clause .= ' AND (folder_name LIKE :search OR description LIKE :search)';
+ }
+ else {//create clause
+ $clause .= ' AND '.$v[0].' = :'.$v[0];
+ }
+ }
+ if ($whereclause == '' && $clause !=''){
+ $whereclause = 'WHERE '.substr($clause, 4);
+ } else {
+ $whereclause .= $clause;
+ }
+}
+
+//Define Query
+if(isset($criterias['totals']) && $criterias['totals'] ==''){
+//Request for total rows
+ $sql = 'SELECT count(*) as count FROM marketing_folders '.$whereclause.'';
+}
+elseif (isset($criterias['list']) && $criterias['list'] =='') {
+ //SQL for list (no paging)
+ $sql = "SELECT
+ mf.*,
+ (SELECT COUNT(*) FROM marketing_files WHERE folder_id = mf.id) as file_count,
+ (SELECT COUNT(*) FROM marketing_folders WHERE parent_id = mf.id) as subfolder_count,
+ CASE
+ WHEN mf.parent_id IS NOT NULL THEN
+ (SELECT folder_name FROM marketing_folders WHERE id = mf.parent_id)
+ ELSE NULL
+ END as parent_folder_name
+ FROM marketing_folders mf
+ " . $whereclause . "
+ ORDER BY mf.folder_name ASC";
+}
+else {
+ //SQL for paging
+ $sql = "SELECT
+ mf.*,
+ (SELECT COUNT(*) FROM marketing_files WHERE folder_id = mf.id) as file_count,
+ (SELECT COUNT(*) FROM marketing_folders WHERE parent_id = mf.id) as subfolder_count,
+ CASE
+ WHEN mf.parent_id IS NOT NULL THEN
+ (SELECT folder_name FROM marketing_folders WHERE id = mf.parent_id)
+ ELSE NULL
+ END as parent_folder_name
+ FROM marketing_folders mf
+ " . $whereclause . "
+ ORDER BY mf.folder_name ASC
+ LIMIT :page,:num_folders";
+}
+
+$stmt = $pdo->prepare($sql);
+
+if (!empty($criterias)){
+ foreach ($criterias as $key => $value){
+ $key_condition = ':'.$key;
+ if (str_contains($whereclause, $key_condition)){
+ if ($key == 'search'){
+ $search_value = '%'.$value.'%';
+ $stmt->bindValue($key, $search_value, PDO::PARAM_STR);
+ }
+ elseif ($key == 'parent_id' && ($value === 'null' || $value === '')) {
+ // Skip binding for NULL parent_id
+ continue;
+ }
+ else {
+ $stmt->bindValue($key, $value, PDO::PARAM_STR);
+ }
+ }
+ }
+}
+
+//Add paging details
+if(isset($criterias['totals']) && $criterias['totals']==''){
+ $stmt->execute();
+ $messages = $stmt->fetch();
+ $messages = $messages[0];
+}
+elseif(isset($criterias['list']) && $criterias['list']==''){
+ //Execute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+else {
+ $current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1;
+ $stmt->bindValue('page', ($current_page - 1) * $page_rows_folders, PDO::PARAM_INT);
+ $stmt->bindValue('num_folders', $page_rows_folders, PDO::PARAM_INT);
+
+ //Execute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+// Check if tree structure is requested
+if (isset($criterias['tree']) && isset($messages) && is_array($messages)) {
+ // Build hierarchical tree structure
+ $messages = buildFolderTree($messages);
+}
+
+//------------------------------------------
+//JSON_ENCODE
+//------------------------------------------
+$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
+
+//Send results
+echo $messages;
\ No newline at end of file
diff --git a/api/v2/get/marketing_tags.php b/api/v2/get/marketing_tags.php
new file mode 100644
index 0000000..fe1d003
--- /dev/null
+++ b/api/v2/get/marketing_tags.php
@@ -0,0 +1,112 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+$whereclause = '';
+
+//NEW ARRAY
+$criterias = [];
+$clause = '';
+
+//Check for $_GET variables and build up clause
+if(isset($get_content) && $get_content!=''){
+ //GET VARIABLES FROM URL
+ $requests = explode("&", $get_content);
+ //Check for keys and values
+ foreach ($requests as $y){
+ $v = explode("=", $y);
+ //INCLUDE VARIABLES IN ARRAY
+ $criterias[$v[0]] = $v[1];
+
+ if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] =='success_msg' || $v[0] == 'action'){
+ //do nothing
+ }
+ elseif ($v[0] == 'search') {
+ $clause .= ' AND tag_name LIKE :search';
+ }
+ elseif ($v[0] == 'used_only') {
+ if ($v[1] === 'true') {
+ $clause .= ' AND id IN (SELECT DISTINCT tag_id FROM marketing_file_tags)';
+ }
+ }
+ else {//create clause
+ $clause .= ' AND '.$v[0].' = :'.$v[0];
+ }
+ }
+ if ($whereclause == '' && $clause !=''){
+ $whereclause = 'WHERE '.substr($clause, 4);
+ } else {
+ $whereclause .= $clause;
+ }
+}
+
+//Set page
+$pagina = 1;
+if(isset($criterias['p']) && $criterias['p'] !='') {
+ $pagina = $criterias['p'];
+}
+
+//check for totals call
+if(isset($criterias['totals'])){
+ $sql = 'SELECT COUNT(*) as found FROM marketing_tags mt '.$whereclause.' ';
+ $stmt = $pdo->prepare($sql);
+
+ // Bind parameters
+ if (!empty($criterias)) {
+ foreach ($criterias as $key => $value) {
+ if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'used_only') {
+ if ($key == 'search') {
+ $stmt->bindValue(':'.$key, '%'.$value.'%');
+ } else {
+ $stmt->bindValue(':'.$key, $value);
+ }
+ }
+ }
+ }
+
+ $stmt->execute();
+ $found = $stmt->fetchColumn();
+ echo $found;
+ exit;
+}
+
+// Main query
+$sql = "SELECT
+ mt.*,
+ COUNT(mft.file_id) as usage_count
+FROM marketing_tags mt
+LEFT JOIN marketing_file_tags mft ON mt.id = mft.tag_id
+" . $whereclause . "
+GROUP BY mt.id
+ORDER BY mt.tag_name ASC";
+
+$stmt = $pdo->prepare($sql);
+
+// Bind parameters
+if (!empty($criterias)) {
+ foreach ($criterias as $key => $value) {
+ if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'used_only') {
+ if ($key == 'search') {
+ $stmt->bindValue(':'.$key, '%'.$value.'%');
+ } else {
+ $stmt->bindValue(':'.$key, $value);
+ }
+ }
+ }
+}
+
+$stmt->execute();
+$marketing_tags = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Return result
+echo json_encode($marketing_tags, JSON_UNESCAPED_UNICODE);
\ No newline at end of file
diff --git a/api/v2/get/payment.php b/api/v2/get/payment.php
index 132e34d..abed788 100644
--- a/api/v2/get/payment.php
+++ b/api/v2/get/payment.php
@@ -49,7 +49,7 @@ if (!$transaction) {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ? LIMIT 1';
$stmt = $pdo->prepare($sql);
-$stmt->execute([$payment_id]);
+$stmt->execute([$transaction['id']]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$item) {
diff --git a/api/v2/get/products_software_upgrade_paths.php b/api/v2/get/products_software_upgrade_paths.php
index 4035243..a114f74 100644
--- a/api/v2/get/products_software_upgrade_paths.php
+++ b/api/v2/get/products_software_upgrade_paths.php
@@ -12,7 +12,7 @@ $pdo = dbConnect($dbname);
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
-list($whereclause,$condition) = getWhereclauselvl2("software_upgrade_paths",$permission,$partner,'get');
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get');
//NEW ARRAY
$criterias = [];
diff --git a/api/v2/get/products_software_versions.php b/api/v2/get/products_software_versions.php
index 31553f2..fcaa325 100644
--- a/api/v2/get/products_software_versions.php
+++ b/api/v2/get/products_software_versions.php
@@ -12,7 +12,7 @@ $pdo = dbConnect($dbname);
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
-list($whereclause,$condition) = getWhereclauselvl2("software_versions",$permission,$partner,'get');
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get');
//NEW ARRAY
$criterias = [];
diff --git a/api/v2/get/report_builder.php b/api/v2/get/report_builder.php
new file mode 100644
index 0000000..03b4fde
--- /dev/null
+++ b/api/v2/get/report_builder.php
@@ -0,0 +1,152 @@
+query("SHOW TABLES");
+ $tables = [];
+ while ($row = $result->fetch(PDO::FETCH_NUM)) {
+ $tables[] = $row[0];
+ }
+
+ $messages = json_encode([
+ 'success' => true,
+ 'tables' => $tables
+ ], JSON_UNESCAPED_UNICODE);
+ } catch (Exception $e) {
+ http_response_code(500);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Failed to fetch tables'
+ ], JSON_UNESCAPED_UNICODE);
+ }
+}
+
+/**
+ * Get columns for a specific table
+ */
+elseif ($action === 'getcolumns') {
+ $table = sanitizeTableName($criterias['table'] ?? '');
+
+ if (!$table) {
+ http_response_code(400);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Invalid table name'
+ ], JSON_UNESCAPED_UNICODE);
+ } else {
+ try {
+ $result = $pdo->query("SHOW COLUMNS FROM `$table`");
+ $columns = [];
+ while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
+ $columns[] = $row['Field'];
+ }
+
+ $messages = json_encode([
+ 'success' => true,
+ 'columns' => $columns
+ ], JSON_UNESCAPED_UNICODE);
+ } catch (Exception $e) {
+ http_response_code(500);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Failed to fetch columns'
+ ], JSON_UNESCAPED_UNICODE);
+ }
+ }
+}
+
+/**
+ * Get table schema information
+ */
+elseif ($action === 'gettableschema') {
+ $table = sanitizeTableName($criterias['table'] ?? '');
+
+ if (!$table) {
+ http_response_code(400);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Invalid table name'
+ ], JSON_UNESCAPED_UNICODE);
+ } else {
+ try {
+ $result = $pdo->query("DESCRIBE `$table`");
+ $schema = [];
+ while ($row = $result->fetch(PDO::FETCH_ASSOC)) {
+ $schema[] = [
+ 'field' => $row['Field'],
+ 'type' => $row['Type'],
+ 'null' => $row['Null'],
+ 'key' => $row['Key'],
+ 'default' => $row['Default'],
+ 'extra' => $row['Extra']
+ ];
+ }
+
+ $messages = json_encode([
+ 'success' => true,
+ 'schema' => $schema
+ ], JSON_UNESCAPED_UNICODE);
+ } catch (Exception $e) {
+ http_response_code(500);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Failed to fetch table schema'
+ ], JSON_UNESCAPED_UNICODE);
+ }
+ }
+}
+
+/**
+ * Invalid or missing action
+ */
+else {
+ http_response_code(400);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Invalid or missing action parameter'
+ ], JSON_UNESCAPED_UNICODE);
+}
+
+// Send results
+echo $messages;
+?>
diff --git a/api/v2/get/role_access_permissions.php b/api/v2/get/role_access_permissions.php
new file mode 100644
index 0000000..096fd5a
--- /dev/null
+++ b/api/v2/get/role_access_permissions.php
@@ -0,0 +1,123 @@
+prepare($sql);
+
+//------------------------------------------
+//Bind to query
+//------------------------------------------
+if (!empty($criterias)){
+ foreach ($criterias as $key => $value){
+ $key_condition = ':'.$key;
+ if (str_contains($sql, $key_condition)){
+ if ($key == 'p'){
+ //Do nothing (bug)
+ }
+ else {
+ $stmt->bindValue($key, $value, PDO::PARAM_STR);
+ }
+ }
+ }
+}
+
+//------------------------------------------
+// Debuglog
+//------------------------------------------
+if (debug){
+ $message = $date.';'.$sql.';'.$username;
+ debuglog($message);
+}
+
+//------------------------------------------
+//Execute Query
+//------------------------------------------
+if(isset($criterias['totals']) && $criterias['totals']==''){
+ $stmt->execute();
+ $messages = $stmt->fetch();
+ $messages = $messages[0];
+}
+else {
+ //Execute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+//------------------------------------------
+//JSON_EnCODE
+//------------------------------------------
+$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
+//------------------------------------------
+//Send results
+//------------------------------------------
+echo $messages;
+
+?>
diff --git a/api/v2/get/service.php b/api/v2/get/service.php
new file mode 100644
index 0000000..3f71c20
--- /dev/null
+++ b/api/v2/get/service.php
@@ -0,0 +1,82 @@
+prepare($sql);
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ echo json_encode($messages);
+
+}
+elseif ($action == 'equipments' && (isset($_GET['serialnumber']) && $_GET['serialnumber'] != '' && !isset($_GET['validate']))) {
+
+ $sql = "SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media, psl.starts_at,psl.expires_at,psl.status as license_status from equipment e LEFT JOIN products p ON e.productrowid = p.rowID LEFT JOIN products_software_licenses psl ON e.sw_version_license = psl.license_key WHERE e.serialnumber = ?";
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$_GET['serialnumber']]);
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ echo json_encode($messages);
+
+}
+elseif ($action == 'equipments' && (isset($_GET['serialnumber']) && $_GET['serialnumber'] != '' && isset($_GET['validate']))){
+
+ $sql = "SELECT count(rowID) as rowID from equipment e WHERE e.serialnumber = ?";
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$_GET['serialnumber']]);
+ $messages = $stmt->fetch();
+
+ if ($messages[0] == 1) {
+ echo json_encode(array('SN'=> TRUE));
+ }
+ else {
+ echo json_encode(array('SN'=> FALSE));
+ }
+
+}
+else {
+ http_response_code(400);
+}
+
+?>
\ No newline at end of file
diff --git a/api/v2/get/software_available.php b/api/v2/get/software_available.php
index 3513575..ca139d1 100644
--- a/api/v2/get/software_available.php
+++ b/api/v2/get/software_available.php
@@ -62,6 +62,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
e.sw_version as current_sw_version,
e.hw_version,
e.sw_version_license,
+ e.sw_version_upgrade,
e.rowID as equipment_rowid
FROM equipment e
JOIN products p ON e.productrowid = p.rowID
@@ -78,6 +79,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$current_sw_version = $equipment_data['current_sw_version'];
$hw_version = $equipment_data['hw_version'];
$sw_version_license = $equipment_data['sw_version_license'];
+ $sw_version_upgrade = $equipment_data['sw_version_upgrade'];
$equipment_rowid = $equipment_data['equipment_rowid'];
if (debug) {
@@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
'product_rowid' => $product_rowid,
'productcode' => $productcode,
'current_sw_version_raw' => $current_sw_version,
- 'hw_version' => $hw_version
+ 'hw_version' => $hw_version,
+ 'sw_version_upgrade' => $sw_version_upgrade
];
}
@@ -119,6 +122,77 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
exit;
}
+ // Check if sw_version_upgrade is set - this overrides normal availability check
+ if (!empty($sw_version_upgrade)) {
+ if (debug) {
+ $debug['sw_version_upgrade_check'] = [
+ 'sw_version_upgrade_id' => $sw_version_upgrade,
+ 'checking_override' => true
+ ];
+ }
+
+ // Check if this version exists and is active
+ $sql = 'SELECT
+ psv.rowID as version_id,
+ psv.version,
+ psv.name,
+ psv.description,
+ psv.mandatory,
+ psv.latest,
+ psv.hw_version,
+ psv.file_path,
+ psv.status
+ FROM products_software_versions psv
+ WHERE psv.rowID = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$sw_version_upgrade]);
+ $upgrade_version = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($upgrade_version && $upgrade_version['status'] == 1) {
+ // Valid override found - check if different from current version
+ $normalized_upgrade_version = strtolower(ltrim($upgrade_version['version'], '0'));
+
+ if (debug) {
+ $debug['sw_version_upgrade_check']['found_version'] = [
+ 'version' => $upgrade_version['version'],
+ 'name' => $upgrade_version['name'],
+ 'normalized' => $normalized_upgrade_version,
+ 'status' => $upgrade_version['status'],
+ 'is_different_from_current' => ($current_sw_version != $normalized_upgrade_version)
+ ];
+ }
+
+ if ($current_sw_version && $normalized_upgrade_version == $current_sw_version) {
+ // Override version is same as current - no upgrade available
+ $software_available = "no";
+ if (debug) {
+ $debug['sw_version_upgrade_check']['decision'] = 'Override version is same as current version';
+ }
+ } else {
+ // Override version is different - upgrade is available
+ $software_available = "yes";
+ if (debug) {
+ $debug['sw_version_upgrade_check']['decision'] = 'Override version is available';
+ }
+ }
+
+ $messages = ["software_available" => $software_available];
+
+ if (debug) {
+ debuglog(json_encode($debug));
+ }
+
+ echo json_encode($messages, JSON_UNESCAPED_UNICODE);
+ exit;
+ } else {
+ // Override version not found or inactive - fall back to standard check
+ if (debug) {
+ $debug['sw_version_upgrade_check']['found_version'] = $upgrade_version ? 'found but inactive' : 'not found';
+ $debug['sw_version_upgrade_check']['decision'] = 'Falling back to standard check';
+ }
+ }
+ }
+
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
$sql = 'SELECT
psv.rowID as version_id,
@@ -161,6 +235,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$debug['decision'] = 'No active software assignments found';
}
} else {
+ $available_upgrades = 0;
$has_priced_options = false;
$has_latest_version_different = false;
@@ -219,8 +294,9 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
FROM products_software_upgrade_paths pup
JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
WHERE pup.to_version_id = ?
- AND LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
- AND pup.is_active = 1';
+ AND (LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
+ OR pup.from_version_id = 9999999)
+ AND pup.is_active = 1';
$stmt = $pdo->prepare($sql);
$stmt->execute([$version['version_id'], $current_sw_version]);
$upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -242,6 +318,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
}
if ($show_version) {
+ $available_upgrades++;
+
//Check if there's a valid license for this upgrade
if ($final_price > 0 && $sw_version_license) {
//Check if the license is valid
@@ -286,23 +364,18 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
}
}
- // Apply the logic:
- // 1. If there are priced options -> "yes"
- // 2. If no priced options but current version != latest flagged version -> "yes"
- // 3. Default -> "no"
- if ($has_priced_options) {
+ // Simple logic: if any upgrades are available to show, return "yes"
+ if ($available_upgrades > 0) {
$software_available = "yes";
- $availability_reason = "Has priced upgrade options available";
- } elseif ($has_latest_version_different) {
- $software_available = "yes";
- $availability_reason = "Has free latest version available";
+ $availability_reason = "Software upgrades available";
} else {
$software_available = "no";
- $availability_reason = "No upgrades available or already on latest";
+ $availability_reason = "No upgrades available";
}
if (debug) {
$debug['final_decision'] = [
+ 'available_upgrades' => $available_upgrades,
'has_priced_options' => $has_priced_options,
'has_latest_version_different' => $has_latest_version_different,
'software_available' => $software_available,
diff --git a/api/v2/get/software_update.php b/api/v2/get/software_update.php
index 2f4696b..a7191c3 100644
--- a/api/v2/get/software_update.php
+++ b/api/v2/get/software_update.php
@@ -55,16 +55,20 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
}
//GET EQUIPMENT AND PRODUCT DATA BASED ON SERIAL NUMBER
- $sql = 'SELECT
+ $sql = "SELECT
p.rowID as product_rowid,
p.productcode,
e.sw_version as current_sw_version,
e.hw_version,
e.sw_version_license,
- e.rowID as equipment_rowid
+ e.sw_version_upgrade,
+ e.rowID as equipment_rowid,
+ partner.*
FROM equipment e
JOIN products p ON e.productrowid = p.rowID
- WHERE e.serialnumber = ?';
+ LEFT JOIN partner ON partner.partnerID = SUBSTRING_INDEX(JSON_UNQUOTE(JSON_EXTRACT(e.accounthierarchy, '$.soldto')), '-', 1)
+ AND partner.is_dealer = 1 AND partner.status = 1
+ WHERE e.serialnumber = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$criterias['sn']]);
$equipment_data = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -77,15 +81,28 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$current_sw_version = $equipment_data['current_sw_version'];
$hw_version = $equipment_data['hw_version'];
$sw_version_license = $equipment_data['sw_version_license'];
+ $sw_version_upgrade = $equipment_data['sw_version_upgrade'];
$equipment_rowid = $equipment_data['equipment_rowid'];
+ $dealer_info = [
+ 'is_dealer' => $equipment_data['is_dealer'] ?? 0,
+ 'name' => $equipment_data['name'] ?? '',
+ 'address' => $equipment_data['address'] ?? '',
+ 'city' => $equipment_data['city'] ?? '',
+ 'postalcode' => $equipment_data['postalcode'] ?? '',
+ 'country' => $equipment_data['country'] ?? '',
+ 'email' => $equipment_data['email'] ?? '',
+ 'phone' => $equipment_data['phone'] ?? ''
+ ];
+
if (debug) {
$debug['equipment_data'] = [
'product_rowid' => $product_rowid,
'productcode' => $productcode,
'current_sw_version_raw' => $current_sw_version,
'hw_version' => $hw_version,
- 'sw_version_license' => $sw_version_license
+ 'sw_version_license' => $sw_version_license,
+ 'sw_version_upgrade' => $sw_version_upgrade
];
}
@@ -119,6 +136,95 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
exit;
}
+ // Check if sw_version_upgrade is set - this overrides normal availability check
+ if (!empty($sw_version_upgrade)) {
+ if (debug) {
+ $debug['sw_version_upgrade_check'] = [
+ 'sw_version_upgrade_id' => $sw_version_upgrade,
+ 'checking_override' => true
+ ];
+ }
+
+ // Check if this version exists and is active
+ $sql = 'SELECT
+ psv.rowID as version_id,
+ psv.version,
+ psv.name,
+ psv.description,
+ psv.mandatory,
+ psv.latest,
+ psv.hw_version,
+ psv.file_path,
+ psv.status
+ FROM products_software_versions psv
+ WHERE psv.rowID = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$sw_version_upgrade]);
+ $upgrade_version = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($upgrade_version && $upgrade_version['status'] == 1) {
+ // Valid override found - check if different from current version
+ $normalized_upgrade_version = strtolower(ltrim($upgrade_version['version'], '0'));
+
+ if (debug) {
+ $debug['sw_version_upgrade_check']['found_version'] = [
+ 'version' => $upgrade_version['version'],
+ 'name' => $upgrade_version['name'],
+ 'normalized' => $normalized_upgrade_version,
+ 'status' => $upgrade_version['status'],
+ 'is_different_from_current' => ($current_sw_version != $normalized_upgrade_version)
+ ];
+ }
+
+ if (!$current_sw_version || $current_sw_version == '' || $normalized_upgrade_version != $current_sw_version) {
+ // Override version is different from current (or no current) - return only this upgrade
+ $output[] = [
+ "productcode" => $productcode,
+ "name" => $upgrade_version['name'] ?? '',
+ "version" => $upgrade_version['version'],
+ "version_id" => $upgrade_version['version_id'],
+ "description" => $upgrade_version['description'] ?? '',
+ "hw_version" => $upgrade_version['hw_version'] ?? '',
+ "mandatory" => $upgrade_version['mandatory'] ?? '',
+ "latest" => $upgrade_version['latest'] ?? '',
+ "software" => $upgrade_version['file_path'] ?? '',
+ "source" => '',
+ "source_type" => '',
+ "price" => '0.00',
+ "currency" => '',
+ "is_current" => false
+ ];
+
+ // Generate download token
+ $download_token = create_download_url_token($criterias['sn'], $upgrade_version['version_id']);
+ $download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download?token='.$download_token;
+ $output[0]['source'] = $download_url;
+ $output[0]['source_type'] = 'token_url';
+
+ if (debug) {
+ $debug['sw_version_upgrade_check']['decision'] = 'Override version returned as only upgrade';
+ $output[0]['_debug'] = $debug;
+ }
+ } else {
+ // Override version is same as current - no upgrades
+ if (debug) {
+ $debug['sw_version_upgrade_check']['decision'] = 'Override version is same as current version - no upgrades';
+ $output = ['message' => 'No upgrades available', 'debug' => $debug];
+ }
+ }
+
+ $messages = $output;
+ echo json_encode($messages, JSON_UNESCAPED_UNICODE);
+ exit;
+ } else {
+ // Override version not found or inactive - fall back to standard check
+ if (debug) {
+ $debug['sw_version_upgrade_check']['found_version'] = $upgrade_version ? 'found but inactive' : 'not found';
+ $debug['sw_version_upgrade_check']['decision'] = 'Falling back to standard check';
+ }
+ }
+ }
+
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
$sql = 'SELECT
psv.rowID as version_id,
@@ -212,16 +318,13 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
$decision_reason = 'Skipped - is current version but no upgrades scenario';
}
} else {
- //Check if this is the current version and should be shown as disabled
- if ($is_current_version && $has_paid_upgrade_from_current && $version['latest'] == 1) {
- //Show current version as disabled only if it's the latest AND there's a paid upgrade available
+ //Check if this is the current version - always show it
+ if ($is_current_version) {
$show_version = true;
$is_current = true;
$final_price = '0.00';
$final_currency = '';
- $decision_reason = 'Showing as CURRENT - is latest version with paid upgrade available';
- } else if ($is_current_version && !($has_paid_upgrade_from_current && $version['latest'] == 1)) {
- $decision_reason = 'Skipped - is current version but not (latest + has_paid_upgrade)';
+ $decision_reason = 'Showing as CURRENT - always show current version';
} else if (!$is_current_version) {
//Check if this version is part of ANY upgrade path system (either FROM or TO)
$sql = 'SELECT COUNT(*) as path_count
@@ -242,26 +345,28 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
} else {
//Part of an upgrade path system
//Only show if there's an explicit path FROM current version TO this version
+ // OR a wildcard path (from_version_id = 9999999)
$sql = 'SELECT pup.price, pup.currency
FROM products_software_upgrade_paths pup
JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
WHERE pup.to_version_id = ?
- AND LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
- AND pup.is_active = 1';
+ AND (LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ?
+ OR pup.from_version_id = 9999999)
+ AND pup.is_active = 1';
$stmt = $pdo->prepare($sql);
$stmt->execute([$version['version_id'], $current_sw_version]);
$upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC);
if ($upgrade_path) {
- //Valid upgrade path found FROM current version
+ //Valid upgrade path found FROM current version or wildcard
$show_version = true;
$final_price = $upgrade_path['price'] ?? '0.00';
$final_currency = $upgrade_path['currency'] ?? '';
- $decision_reason = 'Showing - found upgrade path FROM current (' . $current_sw_version . ') with price: ' . $final_price . ' ' . $final_currency;
+ $decision_reason = 'Showing - found upgrade path FROM current (' . $current_sw_version . ') or wildcard with price: ' . $final_price . ' ' . $final_currency;
} else {
- $decision_reason = 'Skipped - has upgrade paths but none FROM current version (' . $current_sw_version . ')';
+ $decision_reason = 'Skipped - has upgrade paths but none FROM current version (' . $current_sw_version . ') or wildcard';
}
- //If no path from current version exists, don't show (show_version stays false)
+ //If no path from current version or wildcard exists, don't show (show_version stays false)
}
}
}
@@ -310,7 +415,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
}
}
- $output[] = [
+ $entry = [
"productcode" => $productcode,
"name" => $version['name'] ?? '',
"version" => $version['version'],
@@ -324,8 +429,11 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
"source_type" => '',
"price" => $final_price,
"currency" => $final_currency,
- "is_current" => $is_current
+ "is_current" => $is_current,
+ "dealer_info" => $dealer_info
];
+
+ $output[] = $entry;
}
if (debug) {
@@ -360,6 +468,16 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
];
}
+ // Sort output: is_current = true first, then by price low to high
+ usort($output, function($a, $b) {
+ // First priority: is_current (true comes before false)
+ if ($a['is_current'] !== $b['is_current']) {
+ return $b['is_current'] - $a['is_current'];
+ }
+ // Second priority: price (low to high)
+ return floatval($a['price']) - floatval($b['price']);
+ });
+
$messages = $output;
if (debug && !empty($output)) {
diff --git a/api/v2/get/user_credentials.php b/api/v2/get/user_credentials.php
index 8ee04a2..d6ccdba 100644
--- a/api/v2/get/user_credentials.php
+++ b/api/v2/get/user_credentials.php
@@ -19,7 +19,7 @@ if ($stmt->rowCount() == 1) {
//Define User data
$partnerhierarchy = $user_data['partnerhierarchy'];
$permission = userRights($user_data['view']);
- $profile= getProfile($user_data['settings'],$permission);
+ $profile= getUserPermissions($pdo, $user_data['id']); //getProfile($user_data['settings'],$permission);
$username = $user_data['username'];
$useremail = $user_data['email'];
$servicekey = $user_data['service'];
diff --git a/api/v2/get/user_permissions.php b/api/v2/get/user_permissions.php
new file mode 100644
index 0000000..9d6f139
--- /dev/null
+++ b/api/v2/get/user_permissions.php
@@ -0,0 +1,70 @@
+ 'API_INPUT_1','error' => 'user_key is required']));
+}
+
+//GET USER_DATA
+$stmt = $pdo->prepare('SELECT * FROM users WHERE userkey = ?');
+$stmt->execute([$user_key]);
+
+if ($stmt->rowCount() == 1) {
+ //Get results
+ $user_data = $stmt->fetch();
+
+ //GET DATA
+ $user_permissions['id'] = $user_data['id'];
+ $user_permissions['email'] = $user_data['email'];
+ $user_permissions['partnerhierarchy'] = $user_data['partnerhierarchy']; //clean;
+ $user_permissions['permission'] = userRights($user_data['view']);
+ $user_permissions['profile'] = getProfile($user_data['settings'],userRights($user_data['view']));
+
+ //NEW DATA REPLACING PROFILE AND LATER PERMISSION ABOVE
+ $user_permissions['permissions'] = getUserPermissions($pdo, $user_data['id']);
+
+ if (!$user_permissions['permissions']) {
+ http_response_code(404);
+ exit(json_encode(['error_code' => 'API_NOT_FOUND','error' => 'No permissions found']));
+ }
+
+ //+++++++++++++++++++++++++++++++++++++++++++
+ //Return as JSON
+ //+++++++++++++++++++++++++++++++++++++++++++
+ echo json_encode($user_permissions);
+}
+else {
+ http_response_code(404);
+ exit(json_encode(['error_code' => 'API_NOT_FOUND','error' => 'User not found']));
+}
+
+?>
\ No newline at end of file
diff --git a/api/v2/get/user_role_assignments.php b/api/v2/get/user_role_assignments.php
new file mode 100644
index 0000000..f90991e
--- /dev/null
+++ b/api/v2/get/user_role_assignments.php
@@ -0,0 +1,128 @@
+prepare($sql);
+
+//------------------------------------------
+//Bind to query
+//------------------------------------------
+if (!empty($criterias)){
+ foreach ($criterias as $key => $value){
+ $key_condition = ':'.$key;
+ if (str_contains($sql, $key_condition)){
+ if ($key == 'p'){
+ //Do nothing (bug)
+ }
+ else {
+ $stmt->bindValue($key, $value, PDO::PARAM_STR);
+ }
+ }
+ }
+}
+
+//------------------------------------------
+// Debuglog
+//------------------------------------------
+if (debug){
+ $message = $date.';'.$sql.';'.$username;
+ debuglog($message);
+}
+
+//------------------------------------------
+//Execute Query
+//------------------------------------------
+if(isset($criterias['totals']) && $criterias['totals']==''){
+ $stmt->execute();
+ $messages = $stmt->fetch();
+ $messages = $messages[0];
+}
+else {
+ //Execute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+//------------------------------------------
+//JSON_EnCODE
+//------------------------------------------
+$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
+//------------------------------------------
+//Send results
+//------------------------------------------
+echo $messages;
+
+?>
diff --git a/api/v2/get/user_roles.php b/api/v2/get/user_roles.php
new file mode 100644
index 0000000..cb07b3f
--- /dev/null
+++ b/api/v2/get/user_roles.php
@@ -0,0 +1,167 @@
+prepare($sql);
+
+//------------------------------------------
+//Bind to query
+//------------------------------------------
+if (!empty($criterias)){
+ foreach ($criterias as $key => $value){
+ $key_condition = ':'.$key;
+ if (str_contains($sql, $key_condition)){
+ if ($key == 'search'){
+ $search_value = '%'.$value.'%';
+ $stmt->bindValue($key, $search_value, PDO::PARAM_STR);
+ }
+ elseif ($key == 'p'){
+ //Do nothing (bug)
+ }
+ else {
+ $stmt->bindValue($key, $value, PDO::PARAM_STR);
+ }
+ }
+ }
+}
+
+//------------------------------------------
+// Debuglog
+//------------------------------------------
+if (debug){
+ $message = $date.';'.$sql.';'.$username;
+ debuglog($message);
+}
+
+//------------------------------------------
+//Add paging details
+//------------------------------------------
+$page_rows = $page_rows_equipment ?? 20;
+
+if(isset($criterias['totals']) && $criterias['totals']==''){
+ $stmt->execute();
+ $messages = $stmt->fetch();
+ $messages = $messages[0];
+}
+elseif(isset($criterias['all']) && $criterias['all']==''){
+ //Return all records (no paging)
+ $stmt->execute();
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+else {
+ $current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1;
+ $stmt->bindValue('page', ($current_page - 1) * $page_rows, PDO::PARAM_INT);
+ $stmt->bindValue('num_rows', $page_rows, PDO::PARAM_INT);
+ //Execute Query
+ $stmt->execute();
+ //Get results
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+//------------------------------------------
+//JSON_EnCODE
+//------------------------------------------
+$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
+//------------------------------------------
+//Send results
+//------------------------------------------
+echo $messages;
+
+?>
diff --git a/api/v2/post/.DS_Store b/api/v2/post/.DS_Store
deleted file mode 100644
index 5008ddf..0000000
Binary files a/api/v2/post/.DS_Store and /dev/null differ
diff --git a/api/v2/post/access_elements.php b/api/v2/post/access_elements.php
new file mode 100644
index 0000000..e2d768d
--- /dev/null
+++ b/api/v2/post/access_elements.php
@@ -0,0 +1,79 @@
+ $var){
+ if ($key == 'submit' || $key == 'rowID' || str_contains($key, 'old_')){
+ //do nothing
+ }
+ else {
+ $criterias[$key] = $var;
+ $clause .= ' , '.$key.' = ?';
+ $clause_insert .= ' , '.$key.'';
+ $input_insert .= ', ?';
+ $execute_input[]= $var;
+ }
+ }
+}
+
+//CLEAN UP INPUT
+$clause = substr($clause, 2);
+$clause_insert = substr($clause_insert, 2);
+$input_insert = substr($input_insert, 1);
+
+//QUERY AND VERIFY ALLOWED
+if ($command == 'update' && isAllowed('access_element_manage',$profile,$permission,'U') === 1){
+ $sql = 'UPDATE access_elements SET '.$clause.' WHERE rowID = ?';
+ $execute_input[] = $id;
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+}
+elseif ($command == 'insert' && isAllowed('access_element_manage',$profile,$permission,'C') === 1){
+ $sql = 'INSERT INTO access_elements ('.$clause_insert.') VALUES ('.$input_insert.')';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+}
+elseif ($command == 'delete' && isAllowed('access_element_manage',$profile,$permission,'D') === 1){
+ //Delete role permissions using this access element first (foreign key constraint)
+ $stmt = $pdo->prepare('DELETE FROM role_access_permissions WHERE access_id = ?');
+ $stmt->execute([$id]);
+
+ //Delete access element
+ $stmt = $pdo->prepare('DELETE FROM access_elements WHERE rowID = ?');
+ $stmt->execute([$id]);
+}
+
+?>
diff --git a/api/v2/post/accounts.php b/api/v2/post/accounts.php
index 6e7af66..eddcce5 100644
--- a/api/v2/post/accounts.php
+++ b/api/v2/post/accounts.php
@@ -38,7 +38,7 @@ if ($id != ''){
$salesid_new = ((isset($post_content['salesid']) && $post_content['salesid'] != '' && $post_content['salesid'] != $accounthierarchy_old->salesid)? $post_content['salesid'] : $accounthierarchy_old->salesid);
$soldto_new = ((isset($post_content['soldto']) && $post_content['soldto'] != '' && $post_content['soldto'] != $accounthierarchy_old->soldto)? $post_content['soldto'] : $accounthierarchy_old->soldto);
- if ($permission == 3 || $permission == 4){
+ if (getHierarchyLevel($partner) == 1 || getHierarchyLevel($partner) == 0){
//ADMIN ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
diff --git a/api/v2/post/contracts.php b/api/v2/post/contracts.php
index 9ecb7bb..74621e4 100644
--- a/api/v2/post/contracts.php
+++ b/api/v2/post/contracts.php
@@ -58,7 +58,7 @@ if ($id != ''){
$shipto_new = ((isset($post_content['shipto']) && $post_content['shipto'] != '' && $post_content['shipto'] != $contract_old->shipto)? $post_content['shipto'] : $contract_old->shipto);
$location_new = ((isset($post_content['location']) && $post_content['location'] != '' && $post_content['location'] != $contract_old->location)? $post_content['location'] : $contract_old->location);
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -67,7 +67,7 @@ if ($id != ''){
"location"=>$location_new
);
}
- elseif ($permission == 3) {
+ elseif (getHierarchyLevel($partner) == 1) {
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$contract_old->salesid,
@@ -120,7 +120,7 @@ if ($id != ''){
}
else {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
$account = array(
"salesid"=>$post_content['salesid'],
"soldto"=>$post_content['soldto'],
@@ -128,7 +128,7 @@ else {
"location"=>$post_content['location']
);
}
- elseif ($permission == 3){
+ elseif (getHierarchyLevel($partner) == 1){
$account = array(
"salesid"=>$partner->salesid,
"soldto"=>$post_content['soldto'],
@@ -161,7 +161,7 @@ if (isset($post_content['ignore_list'])){
$post_content['ignore_list'] = json_encode($post_content['ignore_list'], JSON_UNESCAPED_UNICODE);
//ONLY ADMINS ARE ALLOWED TO UPDATE IGNORE LIST
- if ($permission != 3 && $permission != 4){
+ if (getHierarchyLevel($partner) != 1 && getHierarchyLevel($partner) != 0){
unset($post_content['ignore_list']);
}
}
diff --git a/api/v2/post/equipments.php b/api/v2/post/equipments.php
index f4d14cd..9057ddb 100644
--- a/api/v2/post/equipments.php
+++ b/api/v2/post/equipments.php
@@ -47,7 +47,7 @@ if ($id != ''){
$owner_equipment = (($equipment_data['createdby'] == $username)? 1 : 0);
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -57,7 +57,7 @@ if ($id != ''){
"section"=>$section_new
);
}
- elseif ($permission == 3) {
+ elseif (getHierarchyLevel($partner) == 1) {
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$equipment_old->salesid,
@@ -79,7 +79,7 @@ if ($id != ''){
}
else {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
$account = array(
"salesid"=>$post_content['salesid'],
"soldto"=>$post_content['soldto'],
@@ -89,7 +89,7 @@ else {
);
}
- elseif ($permission == 3){
+ elseif (getHierarchyLevel($partner) == 1){
$account = array(
"salesid"=>$partner->salesid,
"soldto"=>$post_content['soldto'],
@@ -148,9 +148,9 @@ if ($command == 'update'){
//RESET WARRANTY AND SERVICE DATES WHEN STATUS IS CHANGED TO SEND(3)
if (isset($post_content['status']) && $post_content['status'] == 3 && $equipment_data['status'] != 3)
{
- $post_content['service_date'] = $date;
- $post_content['warranty_date'] = $date;
-
+ $post_content['service_date'] = date("Y-m-d", strtotime("+" . SERVICE_MONTHS . " months"));
+ $post_content['warranty_date'] = date("Y-m-d", strtotime("+" . WARRANTY_MONTHS . " months"));
+ $post_content['order_send_date'] = $date;
}
//UPDATE CHANGELOG BASED ON STATUS CHANGE
if (isset($post_content['status']) && $post_content['status'] != $equipment_data['status'])
@@ -188,8 +188,15 @@ elseif ($command == 'insert'){
$post_content['created'] = $date;
$post_content['createdby'] = $username;
$post_content['accounthierarchy'] = $accounthierarchy;
- $post_content['service_date'] = $date;
- $post_content['warranty_date'] = $date;
+ $post_content['service_date'] = date("Y-m-d", strtotime("+" . SERVICE_MONTHS . " months"));
+ $post_content['warranty_date'] = date("Y-m-d", strtotime("+" . WARRANTY_MONTHS . " months"));
+
+ if (isset($post_content['status']) && $post_content['status'] == 3)
+ {
+ $post_content['order_send_date'] = $date;
+ }
+
+
}
else {
//do nothing
diff --git a/api/v2/post/history.php b/api/v2/post/history.php
index 75eead9..51ed350 100644
--- a/api/v2/post/history.php
+++ b/api/v2/post/history.php
@@ -1,5 +1,6 @@
FROM EXTERNAL APPS
-if (isset($post_content['sn']) && isset($post_content['payload'])){
+if (isset($post_content['sn']) && (isset($post_content['payload']) || isset($post_content['testdetails']))){
+
+ if (!isset($post_content['payload'])) {
+ $post_content['payload'] = $post_content['testdetails'];
+ }
if (!empty($post_content['sn']) && !empty($post_content['payload'])) {
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -39,12 +44,16 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
$updateObject_visual = 0; //update visual inspection object
$sendServiceReport = 0; //send service report via email
$transfercartest = 0; //Update cartest table with incoming data
+ $create_software_license = 0; //Create software license
+
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
//SET DEFAULT PARAMETERS
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
$user = $username;
$account = $partnerhierarchy; //string
- $current_date = date("Y-m-d");
+ $service_date = date("Y-m-d", strtotime("+" . SERVICE_MONTHS . " months"));
+ $warranty_date = date("Y-m-d", strtotime("+" . WARRANTY_MONTHS . " months"));
+ $order_send_date = date("Y-m-d");
$input_type = $post_content['type'];
$testdetails = json_encode($post_content['payload']);
$serial = $post_content['sn'];
@@ -141,12 +150,21 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
$transfercartest = 1;
break;
+ case 12: //customer_consent
+ $historytype = 'Customer_consent';
+ $create_software_license = 1;
+ break;
+
case 'firmware': //update from Portal
$historytype = $HistoryType_2;
$equipmentUpdate = 1;
$servicetoolHistoryUpdate = 1;
$sn_service = $post_content['sn_service'];
break;
+
+ case 'customer': //update from Portal
+ $historytype = 'Customer';
+ break;
default:
$historytype = 'Other';
@@ -155,14 +173,14 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Connect to DB
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
+
//Get whereclause based on serialnumber
$whereclause = checkSerial($serial);
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
//CHECK if EQUIPMENT EXISTS
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
- $sql = "SELECT count(rowID) as total, rowID FROM equipment $whereclause";
+ $sql = "SELECT count(rowID) as total, rowID, hw_version FROM equipment $whereclause";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$total = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -173,9 +191,9 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
// Create equipment when not exist +++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
if ($equipmentCreate == 1 && $total_equipment == 0){
- $sql = 'INSERT INTO equipment (productrowid,created,createdby,status,accounthierarchy,serialnumber,service_date,warranty_date) VALUES (?,?,?,?,?,?,?,?)';
+ $sql = 'INSERT INTO equipment (productrowid,created,createdby,status,accounthierarchy,serialnumber,service_date,warranty_date,order_send_date) VALUES (?,?,?,?,?,?,?,?,?)';
$stmt = $pdo->prepare($sql);
- $stmt->execute([$productrowid,$date,$user,$status0,$account,$serial,$current_date,$current_date]);
+ $stmt->execute([$productrowid,$date,$user,$status0,$account,$serial,$service_date,$warranty_date,$order_send_date]);
$rowID = $pdo->lastInsertId();
}
@@ -209,9 +227,8 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
if ($equipmentUpdate == 1){
//get HW + SW from PortalAPI
if ($post_content['type'] == 'firmware'){
- $test = json_decode($post_content['payload']);
- $hw_version = $test->HW;
- $sw_version = $test->HEX_FW;
+ $hw_version = $post_content['payload']['HW'];
+ $sw_version = $post_content['payload']['HEX_FW'];
}
else {
//GET HW + SW from object
@@ -297,7 +314,7 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
//Update Equipment record
$sql = "UPDATE equipment SET service_date = ? $whereclause";
$stmt = $pdo->prepare($sql);
- $stmt->execute([$current_date]);
+ $stmt->execute([$service_date]);
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -349,6 +366,50 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){
if ($transfercartest == 1){
convertCartest();
}
+
+ // +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ // create software license ++++++++++++++++++++++++++
+ // +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+ if ($create_software_license == 1){
+ // Generate unique license key
+ $license_key = generateUniqueLicenseKey();
+
+ $sw_version_consent = strtolower($post_content['testdetails']['logdetails']['FW'] ?? '');// version_id
+ $eq_version_hw = strtolower($rowID['hw_version'] ?? '');
+
+ //GET VERSION_ID FROM VERSION TABLE
+ $sql = 'SELECT rowID FROM products_software_versions WHERE version = ? and hw_version = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$sw_version_consent, $eq_version_hw]);
+ $version_row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ //GET VERSION_ID or use WILDCARD
+ $sw_version_consent = $version_row['rowID'] ?? '9999999';
+
+ // Create license
+ $sql = 'INSERT INTO products_software_licenses
+ (version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, accounthierarchy,created, createdby)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([
+ $sw_version_consent,
+ 1, // license_type (1 = upgrade)
+ $license_key,
+ 1, // status = active
+ date('Y-m-d H:i:s'),
+ '2099-12-31 23:59:59', // effectively permanent
+ 'Customer_consent',
+ $account,
+ date('Y-m-d H:i:s'),
+ $user
+ ]);
+
+ // Update equipment.sw_version_license
+ $sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$license_key, $rowID]);
+ }
}
else
{
diff --git a/api/v2/post/marketing_delete.php b/api/v2/post/marketing_delete.php
new file mode 100644
index 0000000..ed9036e
--- /dev/null
+++ b/api/v2/post/marketing_delete.php
@@ -0,0 +1,93 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
+
+$file_id = $post_content['file_id'] ?? '';
+
+if (empty($file_id)) {
+ echo json_encode(['error' => 'File ID is required']);
+ exit;
+}
+
+//QUERY AND VERIFY ALLOWED
+if (isAllowed('marketing',$profile,$permission,'D') === 1){
+ // Get file information for cleanup
+ $file_sql = 'SELECT * FROM marketing_files WHERE id = ? AND accounthierarchy LIKE ?';
+ $stmt = $pdo->prepare($file_sql);
+ $stmt->execute([$file_id, '%' . $partner->soldto . '%']);
+ $file_info = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$file_info) {
+ echo json_encode(['error' => 'File not found or access denied']);
+ exit;
+ }
+
+ try {
+ $pdo->beginTransaction();
+
+ // Remove file tags
+ $delete_tags_sql = 'DELETE FROM marketing_file_tags WHERE file_id = ?';
+ $stmt = $pdo->prepare($delete_tags_sql);
+ $stmt->execute([$file_id]);
+
+ // Delete file record
+ $delete_file_sql = 'DELETE FROM marketing_files WHERE id = ? AND accounthierarchy LIKE ?';
+ $stmt = $pdo->prepare($delete_file_sql);
+ $stmt->execute([$file_id, '%' . $partner->soldto . '%']);
+
+ // Delete physical files
+ $base_path = dirname(__FILE__, 4) . "/";
+ $main_file = $base_path . $file_info['file_path'];
+ $thumbnail_file = $file_info['thumbnail_path'] ? $base_path . $file_info['thumbnail_path'] : null;
+
+ $files_deleted = [];
+ $files_failed = [];
+
+ if (file_exists($main_file)) {
+ if (unlink($main_file)) {
+ $files_deleted[] = $file_info['file_path'];
+ } else {
+ $files_failed[] = $file_info['file_path'];
+ }
+ }
+
+ if ($thumbnail_file && file_exists($thumbnail_file)) {
+ if (unlink($thumbnail_file)) {
+ $files_deleted[] = $file_info['thumbnail_path'];
+ } else {
+ $files_failed[] = $file_info['thumbnail_path'];
+ }
+ }
+
+ $pdo->commit();
+
+ echo json_encode([
+ 'success' => true,
+ 'message' => 'File deleted successfully',
+ 'files_deleted' => $files_deleted,
+ 'files_failed' => $files_failed
+ ]);
+
+ } catch (Exception $e) {
+ $pdo->rollback();
+ echo json_encode(['error' => 'Failed to delete file: ' . $e->getMessage()]);
+ }
+} else {
+ echo json_encode(['error' => 'Insufficient permissions']);
+}
+
+?>
\ No newline at end of file
diff --git a/api/v2/post/marketing_folders.php b/api/v2/post/marketing_folders.php
new file mode 100644
index 0000000..ab29f94
--- /dev/null
+++ b/api/v2/post/marketing_folders.php
@@ -0,0 +1,105 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
+
+//BUILD UP PARTNERHIERARCHY FROM USER
+$partner_hierarchy = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
+
+$id = $post_content['id'] ?? ''; //check for rowID
+$command = ($id == '')? 'insert' : 'update'; //IF rowID = empty then INSERT
+if (isset($post_content['delete'])){$command = 'delete';} //change command to delete
+$date = date('Y-m-d H:i:s');
+
+//CREATE EMPTY STRINGS
+$clause = '';
+$clause_insert ='';
+$input_insert = '';
+
+if ($command == 'update'){
+ $post_content['updatedby'] = $username;
+ $post_content['updated'] = $date;
+}
+if ($command == 'insert'){
+ $post_content['createdby'] = $username;
+ $post_content['accounthierarchy'] = $partner_hierarchy;
+}
+
+//CREATE NEW ARRAY AND MAP TO CLAUSE
+if(isset($post_content) && $post_content!=''){
+ foreach ($post_content as $key => $var){
+ if ($key == 'submit' || $key == 'id' || $key == 'delete'){
+ //do nothing
+ }
+ else {
+ // Handle empty parent_id as NULL for foreign key constraint
+ if ($key == 'parent_id' && $var === '') {
+ $var = null;
+ }
+ $criterias[$key] = $var;
+ $clause .= ' , '.$key.' = ?';
+ $clause_insert .= ' , '.$key.'';
+ $input_insert .= ', ?'; // ? for each insert item
+ $execute_input[]= $var; // Build array for input
+ }
+ }
+}
+
+//CLEAN UP INPUT
+$clause = substr($clause, 2); //Clean clause - remove first comma
+$clause_insert = substr($clause_insert, 2); //Clean clause - remove first comma
+$input_insert = substr($input_insert, 1); //Clean clause - remove first comma
+
+//QUERY AND VERIFY ALLOWED
+if ($command == 'update' && isAllowed('marketing',$profile,$permission,'U') === 1){
+ $sql = 'UPDATE marketing_folders SET '.$clause.' WHERE id = ? '.$whereclause.'';
+ $execute_input[] = $id;
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+ echo json_encode(['success' => true, 'message' => 'Folder updated successfully']);
+}
+elseif ($command == 'insert' && isAllowed('marketing',$profile,$permission,'C') === 1){
+ $sql = 'INSERT INTO marketing_folders ('.$clause_insert.') VALUES ('.$input_insert.')';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+ $folder_id = $pdo->lastInsertId();
+ echo json_encode(['success' => true, 'rowID' => $folder_id, 'message' => 'Folder created successfully']);
+}
+elseif ($command == 'delete' && isAllowed('marketing',$profile,$permission,'D') === 1){
+ // Check if folder has subfolders
+ $subfolder_sql = 'SELECT COUNT(*) as count FROM marketing_folders WHERE parent_id = ? AND accounthierarchy LIKE ?';
+ $stmt = $pdo->prepare($subfolder_sql);
+ $stmt->execute([$id, '%' . $partner->soldto . '%']);
+ $subfolder_count = $stmt->fetch()['count'];
+
+ // Check if folder has files
+ $files_sql = 'SELECT COUNT(*) as count FROM marketing_files WHERE folder_id = ? AND accounthierarchy LIKE ?';
+ $stmt = $pdo->prepare($files_sql);
+ $stmt->execute([$id, '%' . $partner->soldto . '%']);
+ $files_count = $stmt->fetch()['count'];
+
+ if ($subfolder_count > 0 || $files_count > 0) {
+ echo json_encode(['error' => 'Cannot delete folder that contains subfolders or files']);
+ } else {
+ $stmt = $pdo->prepare('DELETE FROM marketing_folders WHERE id = ? '.$whereclause.'');
+ $stmt->execute([ $id ]);
+ echo json_encode(['success' => true, 'message' => 'Folder deleted successfully']);
+ }
+} else {
+ echo json_encode(['error' => 'Insufficient permissions or invalid operation']);
+}
+
+?>
\ No newline at end of file
diff --git a/api/v2/post/marketing_update.php b/api/v2/post/marketing_update.php
new file mode 100644
index 0000000..8ede166
--- /dev/null
+++ b/api/v2/post/marketing_update.php
@@ -0,0 +1,94 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
+
+//QUERY AND VERIFY ALLOWED
+if (isAllowed('marketing',$profile,$permission,'U') === 1){
+ // Get JSON input
+ $input = json_decode(file_get_contents('php://input'), true);
+
+ $file_id = $input['file_id'] ?? '';
+
+ if (empty($file_id)) {
+ echo json_encode(['success' => false, 'error' => 'File ID is required']);
+ exit;
+ }
+
+ try {
+ // First verify the file exists and user has access
+ $check_sql = 'SELECT id FROM `marketing_files` WHERE `id` = ?';
+ $check_stmt = $pdo->prepare($check_sql);
+ $check_stmt->execute([$file_id]);
+
+ if ($check_stmt->rowCount() === 0) {
+ echo json_encode(['success' => false, 'error' => 'File not found or access denied']);
+ exit;
+ }
+
+ // Build dynamic UPDATE query for only changed fields
+ $update_fields = [];
+ $update_params = [];
+
+ if (isset($input['title'])) {
+ $update_fields[] = '`title` = ?';
+ $update_params[] = $input['title'];
+ }
+
+ if (isset($input['folder_id'])) {
+ $update_fields[] = '`folder_id` = ?';
+ $update_params[] = $input['folder_id'] ?: null;
+ }
+
+ // Always update updatedby if there are changes
+ if (!empty($update_fields)) {
+ $update_fields[] = '`updatedby` = ?';
+ $update_params[] = $username;
+ $update_params[] = $file_id;
+
+ $update_sql = 'UPDATE `marketing_files` SET ' . implode(', ', $update_fields) . ' WHERE `id` = ?';
+ $stmt = $pdo->prepare($update_sql);
+ $stmt->execute($update_params);
+ }
+
+ // Update tags only if provided
+ if (isset($input['tags'])) {
+ // Remove existing tags
+ $pdo->prepare('DELETE FROM `marketing_file_tags` WHERE `file_id` = ?')->execute([$file_id]);
+
+ // Parse and insert new tags
+ $tags_string = $input['tags'];
+ $tags_array = array_filter(array_map('trim', explode(',', $tags_string)));
+
+ if (!empty($tags_array)) {
+ $tag_sql = 'INSERT IGNORE INTO `marketing_tags` (`tag_name`) VALUES (?)';
+ $tag_stmt = $pdo->prepare($tag_sql);
+
+ $file_tag_sql = 'INSERT INTO `marketing_file_tags` (`file_id`, `tag_id`) SELECT ?, id FROM marketing_tags WHERE tag_name = ?';
+ $file_tag_stmt = $pdo->prepare($file_tag_sql);
+
+ foreach ($tags_array as $tag) {
+ $tag_stmt->execute([$tag]);
+ $file_tag_stmt->execute([$file_id, $tag]);
+ }
+ }
+ }
+
+ echo json_encode(['success' => true, 'message' => 'File updated successfully']);
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => 'Update failed: ' . $e->getMessage()]);
+ }
+} else {
+ echo json_encode(['success' => false, 'error' => 'Insufficient permissions']);
+}
+?>
\ No newline at end of file
diff --git a/api/v2/post/marketing_upload.php b/api/v2/post/marketing_upload.php
new file mode 100644
index 0000000..5055f52
--- /dev/null
+++ b/api/v2/post/marketing_upload.php
@@ -0,0 +1,336 @@
+soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+
+//default whereclause
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
+
+//BUILD UP PARTNERHIERARCHY FROM USER
+$partner_hierarchy = $condition;
+
+//QUERY AND VERIFY ALLOWED
+if (isAllowed('marketing',$profile,$permission,'C') === 1){
+ if (!isset($_FILES['file'])) {
+ echo json_encode(['success' => false, 'error' => 'No file uploaded']);
+ exit;
+ }
+
+ $file = $_FILES['file'];
+ $folder_id = $_POST['folder_id'] ?? '';
+ $tags = isset($_POST['tags']) ? json_decode($_POST['tags'], true) : [];
+ $title = $_POST['title'] ?? pathinfo($file['name'], PATHINFO_FILENAME);
+
+ // Validate file type
+ $allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'mp4', 'mov', 'avi'];
+ $filename = $file['name'];
+ $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+ if (!in_array($ext, $allowedTypes)) {
+ echo json_encode(['success' => false, 'error' => 'Invalid file type. Allowed: ' . implode(', ', $allowedTypes)]);
+ exit;
+ }
+
+ $imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+ $isImage = in_array($ext, $imageTypes);
+
+ // For images over 10MB, automatically compress
+ if ($isImage && $file['size'] > 10000000) {
+ $compressed = compressImage($file['tmp_name'], $ext, 10000000);
+ if ($compressed === false) {
+ echo json_encode(['success' => false, 'error' => 'Failed to compress large image. Please reduce file size manually.']);
+ exit;
+ }
+ // Update file size after compression
+ $file['size'] = filesize($file['tmp_name']);
+ }
+
+ // Non-images must be under 10MB
+ if (!$isImage && $file['size'] > 25000000) {
+ echo json_encode(['success' => false, 'error' => 'File too large. Maximum size is 25MB.']);
+ exit;
+ }
+
+ // Create unique filename
+ $unique_filename = uniqid() . '_' . time() . '.' . $ext;
+ $target_dir = dirname(__FILE__, 4) . "/marketing/uploads/";
+ $target_file = $target_dir . $unique_filename;
+ $logical_path = "marketing/uploads/" . $unique_filename;
+
+ // Ensure upload directory exists
+ if (!file_exists($target_dir)) {
+ mkdir($target_dir, 0755, true);
+ }
+
+ if (move_uploaded_file($file['tmp_name'], $target_file)) {
+ // Generate thumbnail
+ $thumbnail_path = null;
+ $thumb_dir = $target_dir . "thumbs/";
+ if (!file_exists($thumb_dir)) {
+ mkdir($thumb_dir, 0755, true);
+ }
+
+ // Generate thumbnail for images
+ if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
+ $thumbnail_file = $thumb_dir . $unique_filename;
+ if (generateThumbnail($target_file, $thumbnail_file, 200, 200)) {
+ $thumbnail_path = "marketing/uploads/thumbs/" . $unique_filename;
+ }
+ }
+ // Generate thumbnail for videos
+ elseif (in_array($ext, ['mp4', 'mov', 'avi'])) {
+ $thumbnail_filename = pathinfo($unique_filename, PATHINFO_FILENAME) . '.jpg';
+ $thumbnail_file = $thumb_dir . $thumbnail_filename;
+ if (generateVideoThumbnail($target_file, $thumbnail_file)) {
+ $thumbnail_path = "marketing/uploads/thumbs/" . $thumbnail_filename;
+ }
+ }
+
+ // Insert into database
+ $insert_sql = 'INSERT INTO `marketing_files` (`title`, `original_filename`, `file_path`, `thumbnail_path`, `file_type`, `file_size`, `folder_id`, `tags`, `createdby`, `accounthierarchy`) VALUES (?,?,?,?,?,?,?,?,?,?)';
+ $stmt = $pdo->prepare($insert_sql);
+ $stmt->execute([
+ $title,
+ $filename,
+ $logical_path,
+ $thumbnail_path,
+ $ext,
+ $file['size'],
+ $folder_id,
+ json_encode($tags),
+ $username,
+ $partner_hierarchy
+ ]);
+
+ $file_id = $pdo->lastInsertId();
+
+ // Insert tags into separate table
+ if (!empty($tags)) {
+ $tag_sql = 'INSERT IGNORE INTO `marketing_tags` (`tag_name`) VALUES (?)';
+ $tag_stmt = $pdo->prepare($tag_sql);
+
+ $file_tag_sql = 'INSERT INTO `marketing_file_tags` (`file_id`, `tag_id`) SELECT ?, id FROM marketing_tags WHERE tag_name = ?';
+ $file_tag_stmt = $pdo->prepare($file_tag_sql);
+
+ foreach ($tags as $tag) {
+ $tag_stmt->execute([trim($tag)]);
+ $file_tag_stmt->execute([$file_id, trim($tag)]);
+ }
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'file_id' => $file_id,
+ 'path' => $logical_path,
+ 'thumbnail' => $thumbnail_path,
+ 'message' => 'File uploaded successfully'
+ ]);
+
+ } else {
+ echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file']);
+ }
+} else {
+ echo json_encode(['success' => false, 'error' => 'Insufficient permissions']);
+}
+
+// Function to compress large images
+function compressImage($source, $ext, $maxSize) {
+ $info = @getimagesize($source);
+ if ($info === false) return false;
+
+ $mime = $info['mime'];
+
+ // Load image
+ switch ($mime) {
+ case 'image/jpeg':
+ $image = @imagecreatefromjpeg($source);
+ break;
+ case 'image/png':
+ $image = @imagecreatefrompng($source);
+ break;
+ case 'image/gif':
+ $image = @imagecreatefromgif($source);
+ break;
+ case 'image/webp':
+ $image = @imagecreatefromwebp($source);
+ break;
+ default:
+ return false;
+ }
+
+ if ($image === false) return false;
+
+ $width = imagesx($image);
+ $height = imagesy($image);
+
+ // Start with 90% quality and reduce dimensions if needed
+ $quality = 90;
+ $scale = 1.0;
+ $tempFile = $source . '.tmp';
+
+ // Try progressive compression
+ while (true) {
+ // Calculate new dimensions
+ $newWidth = (int)($width * $scale);
+ $newHeight = (int)($height * $scale);
+
+ // Create resized image
+ $resized = imagecreatetruecolor($newWidth, $newHeight);
+
+ // Preserve transparency for PNG/GIF
+ if ($mime === 'image/png' || $mime === 'image/gif') {
+ imagealphablending($resized, false);
+ imagesavealpha($resized, true);
+ $transparent = imagecolorallocatealpha($resized, 255, 255, 255, 127);
+ imagefilledrectangle($resized, 0, 0, $newWidth, $newHeight, $transparent);
+ }
+
+ imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
+
+ // Save with current quality
+ if ($ext === 'jpg' || $ext === 'jpeg') {
+ imagejpeg($resized, $tempFile, $quality);
+ } elseif ($ext === 'png') {
+ // PNG compression level (0-9, where 9 is best compression)
+ $pngQuality = (int)((100 - $quality) / 11);
+ imagepng($resized, $tempFile, $pngQuality);
+ } elseif ($ext === 'webp') {
+ imagewebp($resized, $tempFile, $quality);
+ } else {
+ imagegif($resized, $tempFile);
+ }
+
+ imagedestroy($resized);
+
+ $fileSize = filesize($tempFile);
+
+ // If file is small enough, use it
+ if ($fileSize <= $maxSize) {
+ imagedestroy($image);
+ rename($tempFile, $source);
+ return true;
+ }
+
+ // If we've reduced too much, give up
+ if ($quality < 50 && $scale < 0.5) {
+ imagedestroy($image);
+ @unlink($tempFile);
+ return false;
+ }
+
+ // Reduce quality or scale
+ if ($quality > 50) {
+ $quality -= 10;
+ } else {
+ $scale -= 0.1;
+ }
+ }
+}
+
+// Function to generate thumbnail
+function generateThumbnail($source, $destination, $width, $height) {
+ $info = getimagesize($source);
+ if ($info === false) return false;
+
+ $mime = $info['mime'];
+
+ switch ($mime) {
+ case 'image/jpeg':
+ $image = imagecreatefromjpeg($source);
+ break;
+ case 'image/png':
+ $image = imagecreatefrompng($source);
+ break;
+ case 'image/gif':
+ $image = imagecreatefromgif($source);
+ break;
+ case 'image/webp':
+ $image = imagecreatefromwebp($source);
+ break;
+ default:
+ return false;
+ }
+
+ if ($image === false) return false;
+
+ $original_width = imagesx($image);
+ $original_height = imagesy($image);
+
+ // Calculate aspect ratio
+ $aspect_ratio = $original_width / $original_height;
+
+ if ($width / $height > $aspect_ratio) {
+ $new_width = $height * $aspect_ratio;
+ $new_height = $height;
+ } else {
+ $new_height = $width / $aspect_ratio;
+ $new_width = $width;
+ }
+
+ $thumbnail = imagecreatetruecolor($new_width, $new_height);
+
+ // Preserve transparency
+ imagealphablending($thumbnail, false);
+ imagesavealpha($thumbnail, true);
+ $transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
+ imagefilledrectangle($thumbnail, 0, 0, $new_width, $new_height, $transparent);
+
+ imagecopyresampled($thumbnail, $image, 0, 0, 0, 0, $new_width, $new_height, $original_width, $original_height);
+
+ // Save thumbnail
+ switch ($mime) {
+ case 'image/jpeg':
+ $result = imagejpeg($thumbnail, $destination, 85);
+ break;
+ case 'image/png':
+ $result = imagepng($thumbnail, $destination, 8);
+ break;
+ case 'image/gif':
+ $result = imagegif($thumbnail, $destination);
+ break;
+ case 'image/webp':
+ $result = imagewebp($thumbnail, $destination, 85);
+ break;
+ default:
+ $result = false;
+ }
+
+ imagedestroy($image);
+ imagedestroy($thumbnail);
+
+ return $result;
+}
+
+// Function to generate video thumbnail
+function generateVideoThumbnail($source, $destination) {
+ // Check if ffmpeg is available
+ $ffmpeg = trim(shell_exec('which ffmpeg 2>/dev/null'));
+ if (empty($ffmpeg)) {
+ return false;
+ }
+
+ // Generate thumbnail from video at 1 second mark
+ // -i: input file
+ // -ss: seek to 1 second
+ // -vframes 1: extract one frame
+ // -vf: scale to 200x200 maintaining aspect ratio
+ $command = sprintf(
+ '%s -i %s -ss 00:00:01 -vframes 1 -vf "scale=200:200:force_original_aspect_ratio=decrease" %s 2>&1',
+ escapeshellarg($ffmpeg),
+ escapeshellarg($source),
+ escapeshellarg($destination)
+ );
+
+ exec($command, $output, $return_code);
+
+ return $return_code === 0 && file_exists($destination);
+}
+
+?>
\ No newline at end of file
diff --git a/api/v2/post/payment.php b/api/v2/post/payment.php
index 84b4c54..3f7858b 100644
--- a/api/v2/post/payment.php
+++ b/api/v2/post/payment.php
@@ -1,12 +1,9 @@
fetch(PDO::FETCH_ASSOC);
$has_upgrade_paths = ($path_count_result['path_count'] > 0);
if (!$has_upgrade_paths) {
- // No upgrade paths defined = FREE (lines 240-242 in software_update.php)
+ // No upgrade paths defined = FREE (lines 328-331 in software_update.php)
$final_price = '0.00';
+ if (debug) {
+ debuglog("DEBUG: No upgrade paths defined for version_id $version_id - upgrade is FREE");
+ }
} else {
- // Check for valid upgrade path FROM current version
+ // Check for valid upgrade path FROM current version (same logic as software_update.php lines 335-353)
$sql = 'SELECT pup.price, pup.currency
FROM products_software_upgrade_paths pup
JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID
@@ -91,14 +100,28 @@ if (!$has_upgrade_paths) {
$stmt->execute([$version_id, $current_sw_version]);
$upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC);
+ if (debug) {
+ debuglog("DEBUG: Looking for upgrade path TO version_id=$version_id FROM current_sw_version='$current_sw_version'");
+ debuglog("DEBUG: Upgrade path result: " . json_encode($upgrade_path));
+ }
+
if ($upgrade_path) {
$final_price = $upgrade_path['price'] ?? '0.00';
$final_currency = $upgrade_path['currency'] ?? 'EUR';
+ if (debug) {
+ debuglog("DEBUG: Found upgrade path - price: $final_price $final_currency");
+ }
} else {
// No upgrade path FROM current version
-
+ if (debug) {
+ debuglog("ERROR: No valid upgrade path from current version '$current_sw_version' to version_id $version_id");
+ }
http_response_code(400);
- echo json_encode(['error' => 'No valid upgrade path from current version'], JSON_UNESCAPED_UNICODE);
+ echo json_encode([
+ 'error' => 'No valid upgrade path from current version',
+ 'current_version' => $current_sw_version,
+ 'target_version_id' => $version_id
+ ], JSON_UNESCAPED_UNICODE);
exit;
}
}
@@ -137,67 +160,159 @@ if ($final_price <= 0) {
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
-// STEP 6: DEBUG MODE - Log but continue to real Mollie
+// STEP 6: DEBUG MODE - Log
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
if (debug) {
- debuglog("DEBUG MODE: Creating real Mollie payment for testing");
+ debuglog("DEBUG MODE: Creating $payment_provider payment for testing");
debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price");
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
-// STEP 7: Call Mollie API to create payment
+// STEP 7: Create payment based on provider
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
try {
- // Initialize Mollie
- require dirname(__FILE__, 4).'/initialize.php';
+ // Use payment_amount (with tax) if provided, otherwise use final_price
+ $amount_to_charge = $payment_amount ? (float)$payment_amount : (float)$final_price;
- // Format price for Mollie (must be string with 2 decimals)
- $formatted_price = number_format((float)$final_price, 2, '.', '');
+ // Format price (must be string with 2 decimals)
+ $formatted_price = number_format($amount_to_charge, 2, '.', '');
+
+ if (debug) {
+ debuglog("DEBUG: Item Price (excl. VAT): " . ($item_price ?? $final_price));
+ debuglog("DEBUG: Tax Amount: " . $tax_amount);
+ debuglog("DEBUG: Total Amount (incl. VAT): " . $amount_to_charge);
+ }
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
- // STEP 7A: Generate transaction ID BEFORE creating Mollie payment
+ // STEP 7A: Generate transaction ID BEFORE creating 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
+ // Build URLs
$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);
+ debuglog("DEBUG: Redirect URL: " . $redirect_url);
}
- // Create payment with Mollie
- $payment = $mollie->payments->create([
- 'amount' => [
- 'currency' => $final_currency ?: 'EUR',
- 'value' => "{$formatted_price}"
- ],
- 'description' => "Software upgrade Order #{$txn_id}",
- 'redirectUrl' => "{$redirect_url}",
- 'webhookUrl' => "{$webhook_url}",
- 'metadata' => [
- 'order_id' => $txn_id,
- 'serial_number' => $serial_number,
- 'version_id' => $version_id,
- 'equipment_id' => $equipment_id
- ]
- ]);
+ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
+ // Create payment based on selected provider
+ //+++++++++++++++++++++++++++++++++++++++++++++++++++++
+ if ($payment_provider === 'paypal') {
+ //==========================================
+ // PAYPAL PAYMENT CREATION
+ //==========================================
+ $cancel_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=cancelled&order_id={$txn_id}";
+
+ // Get PayPal access token
+ $access_token = getPayPalAccessToken();
- $mollie_payment_id = $payment->id;
- $checkout_url = $payment->getCheckoutUrl();
+ // Create PayPal order
+ $order_data = [
+ 'intent' => 'CAPTURE',
+ 'purchase_units' => [[
+ 'custom_id' => $txn_id,
+ 'description' => "Software upgrade Order #{$txn_id}",
+ 'amount' => [
+ 'currency_code' => $final_currency ?: 'EUR',
+ 'value' => $formatted_price
+ ],
+ 'payee' => [
+ 'email_address' => email
+ ]
+ ]],
+ 'application_context' => [
+ 'return_url' => $redirect_url,
+ 'cancel_url' => $cancel_url,
+ 'brand_name' => site_name,
+ 'user_action' => 'PAY_NOW'
+ ]
+ ];
+
+ $ch = curl_init(PAYPAL_URL . '/v2/checkout/orders');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($order_data));
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $access_token
+ ]);
+
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($http_code != 200 && $http_code != 201) {
+ debuglog("PayPal API Error: HTTP $http_code - Response: $response");
+ throw new Exception("PayPal order creation failed: HTTP $http_code");
+ }
+
+ $paypal_order = json_decode($response, true);
+ $payment_id = $paypal_order['id'] ?? null;
+
+ // Extract approval URL
+ $checkout_url = '';
+ foreach ($paypal_order['links'] ?? [] as $link) {
+ if ($link['rel'] === 'approve') {
+ $checkout_url = $link['href'];
+ break;
+ }
+ }
+
+ if (!$checkout_url) {
+ throw new Exception("No approval URL received from PayPal");
+ }
+
+ $payment_method_id = 3; // PayPal
+ $payment_metadata = 'paypal_order_id';
+
+ } else {
+ //==========================================
+ // MOLLIE PAYMENT CREATION
+ //==========================================
+ // Initialize Mollie
+ require dirname(__FILE__, 4).'/initialize.php';
+
+ $webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php";
+
+ // Create payment with Mollie
+ $payment = $mollie->payments->create([
+ 'amount' => [
+ 'currency' => $final_currency ?: 'EUR',
+ 'value' => "{$formatted_price}"
+ ],
+ 'description' => "Software upgrade Order #{$txn_id}",
+ 'redirectUrl' => "{$redirect_url}",
+ 'webhookUrl' => "{$webhook_url}",
+ 'metadata' => [
+ 'order_id' => $txn_id,
+ 'serial_number' => $serial_number,
+ 'version_id' => $version_id,
+ 'equipment_id' => $equipment_id
+ ]
+ ]);
+
+ $payment_id = $payment->id;
+ $checkout_url = $payment->getCheckoutUrl();
+
+ if (debug) {
+ debuglog("DEBUG: Mollie payment created successfully");
+ debuglog("DEBUG: Payment ID: $payment_id");
+ debuglog("DEBUG: Redirect URL sent: $redirect_url");
+ debuglog("DEBUG: Checkout URL: $checkout_url");
+ }
+
+ $payment_method_id = 1; // Mollie
+ $payment_metadata = 'mollie_payment_id';
+ }
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: Payment created via $payment_provider");
+ debuglog("DEBUG: Payment ID: $payment_id");
debuglog("DEBUG: Checkout URL: $checkout_url");
}
@@ -213,13 +328,14 @@ try {
// 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, accounthierarchy, created)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
+ $sql = 'INSERT INTO transactions (txn_id, payment_amount, tax_amount, payment_status, payer_email, first_name, last_name,
+ address_street, address_city, address_state, address_zip, address_country, account_id, payment_method, accounthierarchy, created, vat_number)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
- $txn_id, // Use generated transaction ID, not Mollie payment ID
- $final_price,
+ $txn_id,
+ $amount_to_charge, // Total amount including tax
+ $tax_amount, // Tax amount
0, // 0 = pending
$user_data['email'] ?? '',
$first_name,
@@ -230,9 +346,10 @@ try {
$user_data['postal'] ?? '',
$user_data['country'] ?? '',
$serial_number,
- 0, // payment method
+ $payment_method_id, // 0 = Mollie, 1 = PayPal
$partner_product,
- date('Y-m-d H:i:s')
+ date('Y-m-d H:i:s'),
+ $vat_number
]);
// Get the database ID
@@ -245,16 +362,19 @@ try {
'serial_number' => $serial_number,
'equipment_id' => $equipment_id,
'hw_version' => $hw_version,
- 'mollie_payment_id' => $mollie_payment_id // Store Mollie payment ID in options
+ $payment_metadata => $payment_id // Store payment provider ID
], JSON_UNESCAPED_UNICODE);
+ // Use item_price (without VAT) if provided, otherwise use final_price
+ $item_price_to_store = $item_price ? (float)$item_price : (float)$final_price;
+
$sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created)
VALUES (?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([
- $transaction_id, // Use database transaction ID (not txn_id string, not mollie_payment_id)
+ $transaction_id,
$version_id,
- $final_price,
+ $item_price_to_store, // Price without VAT
1,
$item_options,
date('Y-m-d H:i:s')
@@ -265,7 +385,7 @@ try {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
$messages = json_encode([
'checkout_url' => $checkout_url,
- 'payment_id' => $mollie_payment_id
+ 'payment_id' => $payment_id
], JSON_UNESCAPED_UNICODE);
echo $messages;
@@ -275,4 +395,27 @@ try {
exit;
}
+//+++++++++++++++++++++++++++++++++++++++++++++++++++++
+// Helper function to get PayPal access token
+//+++++++++++++++++++++++++++++++++++++++++++++++++++++
+function getPayPalAccessToken() {
+ $ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
+ curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
+
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($http_code != 200) {
+ throw new Exception("Failed to get PayPal access token: HTTP $http_code");
+ }
+
+ $result = json_decode($response, true);
+ return $result['access_token'] ?? '';
+}
+
?>
diff --git a/api/v2/post/placeorder.php b/api/v2/post/placeorder.php
index b6e7b37..306af85 100644
--- a/api/v2/post/placeorder.php
+++ b/api/v2/post/placeorder.php
@@ -46,7 +46,8 @@ if (isset($post_content['cart']) && isset($post_content['checkout_input']) && is
'address_state' => $post_content['customer_details']['address_state'] ?? '',
'address_zip' => $post_content['customer_details']['address_zip'] ?? '',
'address_country' => $post_content['customer_details']['address_country'] ?? '',
- 'address_phone' => $post_content['customer_details']['address_phone'] ?? ''
+ 'address_phone' => $post_content['customer_details']['address_phone'] ?? '',
+ 'vat_number' => $post_content['customer_details']['vat_number'] ?? ''
];
//Initialize calculator
@@ -75,7 +76,7 @@ if (isset($post_content['cart']) && isset($post_content['checkout_input']) && is
$txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5));
// Insert transaction header
- $stmt = $pdo->prepare('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, address_phone, account_id, payment_method, shipping_method, shipping_amount, discount_amount, discount_code, tax_amount,accounthierarchy) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
+ $stmt = $pdo->prepare('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, address_phone, account_id, payment_method, shipping_method, shipping_amount, discount_amount, discount_code, tax_amount,accounthierarchy, vat_number) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
$stmt->execute([
$txn_id,
$total,
@@ -96,7 +97,8 @@ if (isset($post_content['cart']) && isset($post_content['checkout_input']) && is
$discounttotal,
$checkout_input['discount_code'],
$taxtotal,
- $partner_product
+ $partner_product,
+ $customer_details['vat_number']
]);
// Get order ID
$transaction_id = $pdo->lastInsertId();
diff --git a/api/v2/post/products_software_licenses.php b/api/v2/post/products_software_licenses.php
index 1b3a6fe..1282504 100644
--- a/api/v2/post/products_software_licenses.php
+++ b/api/v2/post/products_software_licenses.php
@@ -22,7 +22,7 @@ $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;
+$is_bulk = isset($post_content['bulk']) && ($post_content['bulk'] === "true" || $post_content['bulk'] === true);
$date = date('Y-m-d H:i:s');
@@ -37,12 +37,24 @@ $input_insert = '';
if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$profile,$permission,'C') === 1){
$version_id = $post_content['version_id'] ?? '';
- $serials = $post_content['serials'] ?? [];
+ $serials_input = $post_content['serials'] ?? '';
+
+ // Convert comma-separated string to array and trim whitespace
+ if (is_string($serials_input)) {
+ $serials = array_map('trim', explode(',', $serials_input));
+ } elseif (is_array($serials_input)) {
+ $serials = $serials_input;
+ } else {
+ $serials = [];
+ }
+
$transaction_id = $post_content['transaction_id'] ?? '';
$license_type = $post_content['license_type'] ?? 0;
- $status = $post_content['status'] ?? 0;
+ $status = $post_content['status'] ?? 1;
+ $starts_at = $post_content['starts_at'] ?? date('Y-m-d H:i:s');
+ $expires_at = $post_content['expires_at'] ?? '2099-12-31 23:59:59'; // effectively permanent
- if (empty($version_id) || empty($serials) || !is_array($serials)) {
+ if (empty($version_id) || empty($serials)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid parameters for bulk creation']);
exit;
@@ -51,8 +63,8 @@ if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$
$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 (?, ?, ?, ?, ?, ?, ?, ?)';
+ $sql = 'INSERT INTO products_software_licenses (version_id, license_key, license_type, status, starts_at, expires_at, transaction_id, accounthierarchy, created, createdby)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $pdo->prepare($sql);
$created_count = 0;
@@ -60,13 +72,7 @@ if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$
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)
- );
+ $license_key = generateUniqueLicenseKey();
try {
$stmt->execute([
@@ -74,6 +80,8 @@ if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$
$license_key,
$license_type,
$status,
+ $starts_at,
+ $expires_at,
$transaction_id,
$accounthierarchy,
$date,
@@ -81,9 +89,9 @@ if ($command == 'insert' && $is_bulk && isAllowed('products_software_licenses',$
]);
// Assign license to equipment if serial number exists
- $eq_sql = 'UPDATE equipment SET sw_version_license = ? WHERE serialnumber = ? AND accounthierarchy LIKE ?';
+ $eq_sql = 'UPDATE equipment SET sw_version_license = ? WHERE serialnumber = ? ';
$eq_stmt = $pdo->prepare($eq_sql);
- $eq_stmt->execute([$license_key, $serial, '%'.$partner->soldto.'%']);
+ $eq_stmt->execute([$license_key, $serial]);
$created_count++;
} catch (Exception $e) {
@@ -104,17 +112,8 @@ if ($command == 'update'){
$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)
- );
- }
-
+ // Generate UUID for license key
+ $post_content['license_key'] = generateUniqueLicenseKey();
$post_content['created'] = $date;
$post_content['createdby'] = $username;
$post_content['accounthierarchy'] = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
diff --git a/api/v2/post/products_software_upgrade_paths.php b/api/v2/post/products_software_upgrade_paths.php
index 6b6f9a4..6ce616f 100644
--- a/api/v2/post/products_software_upgrade_paths.php
+++ b/api/v2/post/products_software_upgrade_paths.php
@@ -14,7 +14,7 @@ $post_content = json_decode($input,true);
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//default whereclause
-list($whereclause,$condition) = getWhereclauselvl2("software_upgrade_paths",$permission,$partner,'');
+list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
//SET PARAMETERS FOR QUERY
$id = $post_content['rowID'] ?? ''; //check for rowID
diff --git a/api/v2/post/report_builder.php b/api/v2/post/report_builder.php
new file mode 100644
index 0000000..9d6516e
--- /dev/null
+++ b/api/v2/post/report_builder.php
@@ -0,0 +1,124 @@
+ false,
+ 'message' => 'Query parameter is required'
+ ], JSON_UNESCAPED_UNICODE);
+ }
+ // Security check: only allow SELECT queries
+ elseif (!isSelectQuery($query)) {
+ http_response_code(400);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Only SELECT queries are allowed'
+ ], JSON_UNESCAPED_UNICODE);
+ } else {
+ try {
+ // Execute the query
+ $stmt = $pdo->query($query);
+
+ // Fetch all results
+ $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ // Get row count
+ $rowCount = count($results);
+
+ // Limit results to prevent memory issues
+ $maxResults = 5000;
+ if ($rowCount > $maxResults) {
+ $results = array_slice($results, 0, $maxResults);
+ $message = "Query executed successfully. Showing first $maxResults of $rowCount rows.";
+ } else {
+ $message = "Query executed successfully. $rowCount rows returned.";
+ }
+
+ $messages = json_encode([
+ 'success' => true,
+ 'results' => $results,
+ 'rowCount' => $rowCount,
+ 'message' => $message
+ ], JSON_UNESCAPED_UNICODE);
+
+ } catch (PDOException $e) {
+ http_response_code(400);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Query execution failed: ' . $e->getMessage()
+ ], JSON_UNESCAPED_UNICODE);
+ }
+ }
+}
+
+/**
+ * Invalid or missing action
+ */
+else {
+ http_response_code(400);
+ $messages = json_encode([
+ 'success' => false,
+ 'message' => 'Invalid or missing action parameter'
+ ], JSON_UNESCAPED_UNICODE);
+}
+
+// Send results
+echo $messages;
+?>
diff --git a/api/v2/post/role_access_permissions.php b/api/v2/post/role_access_permissions.php
new file mode 100644
index 0000000..c064dc2
--- /dev/null
+++ b/api/v2/post/role_access_permissions.php
@@ -0,0 +1,75 @@
+ $var){
+ if ($key == 'submit' || $key == 'rowID' || str_contains($key, 'old_')){
+ //do nothing
+ }
+ else {
+ $criterias[$key] = $var;
+ $clause .= ' , '.$key.' = ?';
+ $clause_insert .= ' , '.$key.'';
+ $input_insert .= ', ?';
+ $execute_input[]= $var;
+ }
+ }
+}
+
+//CLEAN UP INPUT
+$clause = substr($clause, 2);
+$clause_insert = substr($clause_insert, 2);
+$input_insert = substr($input_insert, 1);
+
+//QUERY AND VERIFY ALLOWED
+if ($command == 'update' && isAllowed('user_role_manage',$profile,$permission,'U') === 1){
+ $sql = 'UPDATE role_access_permissions SET '.$clause.' WHERE rowID = ?';
+ $execute_input[] = $id;
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+}
+elseif ($command == 'insert' && isAllowed('user_role_manage',$profile,$permission,'C') === 1){
+ $sql = 'INSERT INTO role_access_permissions ('.$clause_insert.') VALUES ('.$input_insert.')';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+}
+elseif ($command == 'delete' && isAllowed('user_role_manage',$profile,$permission,'D') === 1){
+ //Delete permission
+ $stmt = $pdo->prepare('DELETE FROM role_access_permissions WHERE rowID = ?');
+ $stmt->execute([$id]);
+}
+
+?>
diff --git a/api/v2/post/user_role_assignments.php b/api/v2/post/user_role_assignments.php
new file mode 100644
index 0000000..cd663d9
--- /dev/null
+++ b/api/v2/post/user_role_assignments.php
@@ -0,0 +1,141 @@
+prepare('SELECT role_id, rowID FROM user_role_assignments WHERE user_id = ? AND is_active = 1');
+ $stmt->execute([$user_id]);
+ $current_roles = [];
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)){
+ $current_roles[$row['role_id']] = $row['rowID'];
+ }
+
+ //Remove roles that are no longer selected (soft delete)
+ foreach ($current_roles as $role_id => $assignment_id){
+ if (!in_array($role_id, $selected_roles)){
+ $stmt = $pdo->prepare('UPDATE user_role_assignments SET is_active = 0, updatedby = ?, updated = ? WHERE rowID = ?');
+ $stmt->execute([$username, $date, $assignment_id]);
+ }
+ }
+
+ //Add new roles that are selected but not currently assigned
+ foreach ($selected_roles as $role_id){
+ if (!array_key_exists($role_id, $current_roles)){
+ //Check if this user-role combination existed before (inactive)
+ $stmt = $pdo->prepare('SELECT rowID FROM user_role_assignments WHERE user_id = ? AND role_id = ? AND is_active = 0 LIMIT 1');
+ $stmt->execute([$user_id, $role_id]);
+ $existing = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($existing){
+ //Reactivate existing assignment
+ $stmt = $pdo->prepare('UPDATE user_role_assignments SET is_active = 1, assigned_by = ?, assigned_at = ?, updatedby = ?, updated = ? WHERE rowID = ?');
+ $stmt->execute([$username, $date, $username, $date, $existing['rowID']]);
+ } else {
+ //Create new assignment
+ $stmt = $pdo->prepare('INSERT INTO user_role_assignments (user_id, role_id, is_active, assigned_by, assigned_at, created, createdby) VALUES (?, ?, 1, ?, ?, ?, ?)');
+ $stmt->execute([$user_id, $role_id, $username, $date, $date, $userkey]);
+ }
+ }
+ }
+}
+//------------------------------------------
+// SINGLE OPERATIONS (for backward compatibility or direct API calls)
+//------------------------------------------
+else {
+ $command = ($id == '')? 'insert' : 'update';
+ if (isset($post_content['delete'])){$command = 'delete';}
+
+ //CREATE EMPTY STRINGS
+ $clause = '';
+ $clause_insert ='';
+ $input_insert = '';
+ $execute_input = [];
+ $criterias = [];
+
+ //ADD STANDARD PARAMETERS TO ARRAY BASED ON INSERT OR UPDATE
+ if ($command == 'update'){
+ $post_content['updatedby'] = $username;
+ $post_content['updated'] = $date;
+ }
+ elseif ($command == 'insert'){
+ $post_content['created'] = $date;
+ $post_content['createdby'] = $username;
+ $post_content['assigned_by'] = $username;
+ $post_content['assigned_at'] = $date;
+ }
+
+ //CREAT NEW ARRAY AND MAP TO CLAUSE
+ if(isset($post_content) && $post_content!=''){
+ foreach ($post_content as $key => $var){
+ if ($key == 'submit' || $key == 'rowID' || $key == 'delete' || $key == 'batch_update' || str_contains($key, 'old_')){
+ //do nothing
+ }
+ else {
+ $criterias[$key] = $var;
+ $clause .= ' , '.$key.' = ?';
+ $clause_insert .= ' , '.$key.'';
+ $input_insert .= ', ?';
+ $execute_input[]= $var;
+ }
+ }
+ }
+
+ //CLEAN UP INPUT
+ $clause = substr($clause, 2);
+ $clause_insert = substr($clause_insert, 2);
+ $input_insert = substr($input_insert, 1);
+
+ //QUERY AND VERIFY ALLOWED
+ if ($command == 'update' && isAllowed('user_manage',$profile,$permission,'U') === 1){
+ $sql = 'UPDATE user_role_assignments SET '.$clause.' WHERE rowID = ?';
+ $execute_input[] = $id;
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+ }
+ elseif ($command == 'insert' && isAllowed('user_manage',$profile,$permission,'C') === 1){
+ //Check if this user-role combination already exists (including inactive ones)
+ $stmt = $pdo->prepare('SELECT rowID, is_active FROM user_role_assignments WHERE user_id = ? AND role_id = ? LIMIT 1');
+ $stmt->execute([$post_content['user_id'], $post_content['role_id']]);
+ $existing = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($existing){
+ //If exists but inactive, reactivate it
+ if ($existing['is_active'] == 0){
+ $stmt = $pdo->prepare('UPDATE user_role_assignments SET is_active = 1, assigned_by = ?, assigned_at = ?, updatedby = ?, updated = ? WHERE rowID = ?');
+ $stmt->execute([$username, $date, $username, $date, $existing['rowID']]);
+ }
+ //If already active, do nothing (or could throw an error)
+ } else {
+ //Insert new assignment
+ $sql = 'INSERT INTO user_role_assignments ('.$clause_insert.') VALUES ('.$input_insert.')';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+ }
+ }
+ elseif ($command == 'delete' && isAllowed('user_manage',$profile,$permission,'D') === 1){
+ //Soft delete by setting is_active to 0
+ $stmt = $pdo->prepare('UPDATE user_role_assignments SET is_active = 0, updatedby = ?, updated = ? WHERE rowID = ?');
+ $stmt->execute([$username, $date, $id]);
+ }
+}
+
+?>
diff --git a/api/v2/post/user_roles.php b/api/v2/post/user_roles.php
new file mode 100644
index 0000000..da38722
--- /dev/null
+++ b/api/v2/post/user_roles.php
@@ -0,0 +1,123 @@
+ $var){
+ if ($key == 'submit' || $key == 'rowID' || $key == 'permissions' || str_contains($key, 'old_')){
+ //do nothing
+ }
+ else {
+ $criterias[$key] = $var;
+ $clause .= ' , '.$key.' = ?';
+ $clause_insert .= ' , '.$key.'';
+ $input_insert .= ', ?';
+ $execute_input[]= $var;
+ }
+ }
+}
+
+//CLEAN UP INPUT
+$clause = substr($clause, 2);
+$clause_insert = substr($clause_insert, 2);
+$input_insert = substr($input_insert, 1);
+
+//QUERY AND VERIFY ALLOWED
+if ($command == 'update' && isAllowed('user_role_manage',$profile,$permission,'U') === 1){
+ $sql = 'UPDATE user_roles SET '.$clause.' WHERE rowID = ?';
+ $execute_input[] = $id;
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+
+ //Handle permissions update
+ if (isset($post_content['permissions'])){
+ //First delete all existing permissions for this role
+ $stmt = $pdo->prepare('DELETE FROM role_access_permissions WHERE role_id = ?');
+ $stmt->execute([$id]);
+
+ //Insert new permissions
+ foreach ($post_content['permissions'] as $access_id => $perms){
+ $can_create = isset($perms['can_create']) ? 1 : 0;
+ $can_read = isset($perms['can_read']) ? 1 : 0;
+ $can_update = isset($perms['can_update']) ? 1 : 0;
+ $can_delete = isset($perms['can_delete']) ? 1 : 0;
+
+ //Only insert if at least one permission is set
+ if ($can_create || $can_read || $can_update || $can_delete){
+ $stmt = $pdo->prepare('INSERT INTO role_access_permissions (role_id, access_id, can_create, can_read, can_update, can_delete, created, createdby) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
+ $stmt->execute([$id, $access_id, $can_create, $can_read, $can_update, $can_delete, $date, $userkey]);
+ }
+ }
+ }
+}
+elseif ($command == 'insert' && isAllowed('user_role_manage',$profile,$permission,'C') === 1){
+ $sql = 'INSERT INTO user_roles ('.$clause_insert.') VALUES ('.$input_insert.')';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute($execute_input);
+
+ //Get the new role ID
+ $new_role_id = $pdo->lastInsertId();
+
+ //Handle permissions for new role
+ if (isset($post_content['permissions'])){
+ foreach ($post_content['permissions'] as $access_id => $perms){
+ $can_create = isset($perms['can_create']) ? 1 : 0;
+ $can_read = isset($perms['can_read']) ? 1 : 0;
+ $can_update = isset($perms['can_update']) ? 1 : 0;
+ $can_delete = isset($perms['can_delete']) ? 1 : 0;
+
+ //Only insert if at least one permission is set
+ if ($can_create || $can_read || $can_update || $can_delete){
+ $stmt = $pdo->prepare('INSERT INTO role_access_permissions (role_id, access_id, can_create, can_read, can_update, can_delete, created, createdby) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
+ $stmt->execute([$new_role_id, $access_id, $can_create, $can_read, $can_update, $can_delete, $date, $userkey]);
+ }
+ }
+ }
+}
+elseif ($command == 'delete' && isAllowed('user_role_manage',$profile,$permission,'D') === 1){
+ //Delete role permissions first (foreign key constraint)
+ $stmt = $pdo->prepare('DELETE FROM role_access_permissions WHERE role_id = ?');
+ $stmt->execute([$id]);
+
+ //Delete user role assignments
+ $stmt = $pdo->prepare('DELETE FROM user_role_assignments WHERE role_id = ?');
+ $stmt->execute([$id]);
+
+ //Delete role
+ $stmt = $pdo->prepare('DELETE FROM user_roles WHERE rowID = ?');
+ $stmt->execute([$id]);
+}
+
+?>
diff --git a/api/v2/post/users.php b/api/v2/post/users.php
index fba231a..175e8f3 100644
--- a/api/v2/post/users.php
+++ b/api/v2/post/users.php
@@ -44,12 +44,13 @@ $user_name_old = $user_data['username'];
$view_old = $user_data['view'];
$partnerhierarchy_old = json_decode($user_data['partnerhierarchy']);
-$salesid_new = ((isset($post_content['salesid']) && $post_content['salesid'] != '' && $post_content['salesid'] != $partnerhierarchy_old->salesid)? $post_content['salesid'] : $partnerhierarchy_old->salesid);
-$soldto_new = ((isset($post_content['soldto']) && $post_content['soldto'] != '' && $post_content['soldto'] != $partnerhierarchy_old->soldto)? $post_content['soldto'] : $partnerhierarchy_old->soldto);
-$shipto_new = ((isset($post_content['shipto']) && $post_content['shipto'] != '' && $post_content['shipto'] != $partnerhierarchy_old->shipto)? $post_content['shipto'] : $partnerhierarchy_old->shipto);
-$location_new = ((isset($post_content['location']) && $post_content['location'] != '' && $post_content['location'] != $partnerhierarchy_old->location)? $post_content['location'] : $partnerhierarchy_old->location);
+// Allow clearing values by checking if key exists (even if empty)
+$salesid_new = (array_key_exists('salesid', $post_content)) ? $post_content['salesid'] : ($partnerhierarchy_old->salesid ?? '');
+$soldto_new = (array_key_exists('soldto', $post_content)) ? $post_content['soldto'] : ($partnerhierarchy_old->soldto ?? '');
+$shipto_new = (array_key_exists('shipto', $post_content)) ? $post_content['shipto'] : ($partnerhierarchy_old->shipto ?? '');
+$location_new = (array_key_exists('location', $post_content)) ? $post_content['location'] : ($partnerhierarchy_old->location ?? '');
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$salesid_new,
@@ -57,7 +58,7 @@ $location_new = ((isset($post_content['location']) && $post_content['location']
"shipto"=>$shipto_new,
"location"=>$location_new
);
- }elseif ($permission == 3) {
+ }elseif (getHierarchyLevel($partner) == 1) {
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$partner->salesid,
@@ -76,7 +77,7 @@ $location_new = ((isset($post_content['location']) && $post_content['location']
}
} elseif ($command == 'insert') {
//ID is empty => INSERT / NEW RECORD
- if ($permission == 4){
+ if (getHierarchyLevel($partner) == 0){
//ADMIN+ ONLY ARE ALLOWED TO CHANGE SALES AND SOLD
$account = array(
"salesid"=>$post_content['salesid'],
@@ -85,7 +86,7 @@ $location_new = ((isset($post_content['location']) && $post_content['location']
"location"=>$post_content['location']
);
}
- elseif ($permission == 3){
+ elseif (getHierarchyLevel($partner) == 1){
//ADMIN ONLY ARE ALLOWED TO CHANGE SOLD
$account = array(
"salesid"=>$partner->salesid,
@@ -153,12 +154,15 @@ else {
//+++++++++++++++++++++++++++++++++++++++++++++
//RESET VIEW/PERMISSION BASED ON USER PERMISSION
//+++++++++++++++++++++++++++++++++++++++++++++
+
+$hierarchy_level = getHierarchyLevel($partner);
+
if($post_content['view']){
- switch ($permission) {
- case '4':
+ switch ($hierarchy_level) {
+ case '0':
//ADMIN+ no override
break;
- case '3':
+ case '1':
//ADMINS cannot set ADMIN+ => reset to admin
$post_content['view'] = ($post_content['view'] == 5) ? 4 : $post_content['view'];
break;
diff --git a/assets/admin.js b/assets/admin.js
index b3869a2..31e7cde 100644
--- a/assets/admin.js
+++ b/assets/admin.js
@@ -20,6 +20,66 @@ document.querySelector('.responsive-toggle').onclick = event => {
localStorage.setItem('admin_menu', 'closed');
}
};
+
+// Menu header collapse/expand functionality
+document.querySelectorAll('aside .menu-header').forEach(header => {
+ header.addEventListener('click', function(event) {
+ event.preventDefault();
+
+ // Toggle expanded state
+ this.classList.toggle('expanded');
+
+ // Find the next sibling .sub element and toggle display
+ const submenu = this.nextElementSibling;
+ if (submenu && submenu.classList.contains('sub')) {
+ submenu.classList.toggle('expanded');
+ // Update inline style for display
+ submenu.style.display = submenu.classList.contains('expanded') ? 'flex' : 'none';
+ }
+
+ // Rotate chevron
+ const chevron = this.querySelector('.menu-chevron');
+ if (chevron) {
+ chevron.style.transform = this.classList.contains('expanded') ? 'rotate(180deg)' : 'rotate(0deg)';
+ }
+
+ // Store expanded state in localStorage for persistence
+ const section = this.dataset.section;
+ if (section) {
+ const expandedSections = JSON.parse(localStorage.getItem('menu_expanded') || '{}');
+ expandedSections[section] = this.classList.contains('expanded');
+ localStorage.setItem('menu_expanded', JSON.stringify(expandedSections));
+ }
+ });
+});
+
+// Restore menu expanded states from localStorage on page load
+(function restoreMenuState() {
+ const expandedSections = JSON.parse(localStorage.getItem('menu_expanded') || '{}');
+
+ document.querySelectorAll('aside .menu-header').forEach(header => {
+ const section = header.dataset.section;
+ const submenu = header.nextElementSibling;
+ const chevron = header.querySelector('.menu-chevron');
+
+ // If explicitly saved as expanded, apply it
+ if (section && expandedSections[section] === true) {
+ header.classList.add('expanded');
+ if (submenu && submenu.classList.contains('sub')) {
+ submenu.classList.add('expanded');
+ submenu.style.display = 'flex';
+ }
+ if (chevron) chevron.style.transform = 'rotate(180deg)';
+ }
+ // If has selected child, always expand (override localStorage)
+ if (submenu && submenu.querySelector('a.selected')) {
+ header.classList.add('expanded');
+ submenu.classList.add('expanded');
+ submenu.style.display = 'flex';
+ if (chevron) chevron.style.transform = 'rotate(180deg)';
+ }
+ });
+})();
document.querySelectorAll('.tabs a').forEach((element, index) => {
element.onclick = event => {
event.preventDefault();
@@ -1158,14 +1218,22 @@ function sortTextVal(a, b) {
// Print DIV
//------------------------------------------
function printDiv(div) {
-var divContents = document.getElementById(div).innerHTML;
-var a = window.open('', '', '');
-a.document.write('');
-a.document.write(' ');
-a.document.write(divContents);
-a.document.write('');
-a.document.close();
-a.print();
+ var divContents = document.getElementById(div).innerHTML;
+ var printWindow = window.open('', '', 'height=600,width=800');
+ printWindow.document.write('
Print ');
+ printWindow.document.write('');
+ printWindow.document.write('');
+ printWindow.document.write(divContents);
+ printWindow.document.write('');
+ printWindow.document.close();
+ printWindow.focus();
+ printWindow.print();
+ printWindow.close();
}
//------------------------------------------
diff --git a/assets/database/marketing_install.sql b/assets/database/marketing_install.sql
new file mode 100644
index 0000000..7fb34e3
--- /dev/null
+++ b/assets/database/marketing_install.sql
@@ -0,0 +1,114 @@
+-- Marketing System Database Tables
+-- Run this script to create the necessary tables for the marketing file management system
+--
+-- Usage: Import this file into your MySQL database or run the commands individually
+-- Make sure to select the correct database before running these commands
+
+-- Disable foreign key checks temporarily to avoid constraint errors
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- Create marketing_folders table
+CREATE TABLE IF NOT EXISTS `marketing_folders` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `folder_name` varchar(255) NOT NULL,
+ `parent_id` int(11) DEFAULT NULL,
+ `description` text DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `createdby` varchar(100) DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ `updatedby` varchar(100) DEFAULT NULL,
+ `accounthierarchy` text DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `parent_id` (`parent_id`),
+ KEY `accounthierarchy_idx` (`accounthierarchy`(100))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Create marketing_files table
+CREATE TABLE IF NOT EXISTS `marketing_files` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `title` varchar(255) NOT NULL,
+ `original_filename` varchar(255) NOT NULL,
+ `file_path` varchar(500) NOT NULL,
+ `thumbnail_path` varchar(500) DEFAULT NULL,
+ `file_type` varchar(10) NOT NULL,
+ `file_size` bigint(20) NOT NULL DEFAULT 0,
+ `folder_id` int(11) DEFAULT NULL,
+ `tags` json DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `createdby` varchar(100) DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ `updatedby` varchar(100) DEFAULT NULL,
+ `accounthierarchy` text DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `folder_id` (`folder_id`),
+ KEY `file_type` (`file_type`),
+ KEY `accounthierarchy_idx` (`accounthierarchy`(100)),
+ KEY `created_idx` (`created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Create marketing_tags table
+CREATE TABLE IF NOT EXISTS `marketing_tags` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `tag_name` varchar(100) NOT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `tag_name` (`tag_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Create marketing_file_tags junction table
+CREATE TABLE IF NOT EXISTS `marketing_file_tags` (
+ `file_id` int(11) NOT NULL,
+ `tag_id` int(11) NOT NULL,
+ PRIMARY KEY (`file_id`, `tag_id`),
+ KEY `file_id` (`file_id`),
+ KEY `tag_id` (`tag_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Add foreign key constraints after all tables are created
+ALTER TABLE `marketing_folders`
+ADD CONSTRAINT `fk_marketing_folders_parent`
+FOREIGN KEY (`parent_id`) REFERENCES `marketing_folders`(`id`) ON DELETE CASCADE;
+
+ALTER TABLE `marketing_files`
+ADD CONSTRAINT `fk_marketing_files_folder`
+FOREIGN KEY (`folder_id`) REFERENCES `marketing_folders`(`id`) ON DELETE SET NULL;
+
+ALTER TABLE `marketing_file_tags`
+ADD CONSTRAINT `fk_marketing_file_tags_file`
+FOREIGN KEY (`file_id`) REFERENCES `marketing_files`(`id`) ON DELETE CASCADE;
+
+ALTER TABLE `marketing_file_tags`
+ADD CONSTRAINT `fk_marketing_file_tags_tag`
+FOREIGN KEY (`tag_id`) REFERENCES `marketing_tags`(`id`) ON DELETE CASCADE;
+
+-- Re-enable foreign key checks
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- Insert some default sample data (optional)
+-- Uncomment the lines below if you want to start with sample folders and tags
+
+-- INSERT INTO `marketing_folders` (`folder_name`, `description`, `createdby`) VALUES
+-- ('Product Brochures', 'Marketing brochures and product information', 'system'),
+-- ('Technical Specifications', 'Technical documentation and specifications', 'system'),
+-- ('Images', 'Product images and photos', 'system'),
+-- ('Videos', 'Product videos and demonstrations', 'system');
+
+-- INSERT INTO `marketing_tags` (`tag_name`) VALUES
+-- ('brochure'),
+-- ('specification'),
+-- ('manual'),
+-- ('image'),
+-- ('video'),
+-- ('product'),
+-- ('marketing'),
+-- ('technical');
+
+-- Create upload directories (Note: This requires manual creation on file system)
+-- Create the following directories in your web server:
+-- - ./marketing/uploads/
+-- - ./marketing/uploads/thumbs/
+--
+-- Linux/macOS commands:
+-- mkdir -p marketing/uploads/thumbs
+-- chmod 755 marketing/uploads
+-- chmod 755 marketing/uploads/thumbs
\ No newline at end of file
diff --git a/assets/database/migration_profiles_to_rbac.sql b/assets/database/migration_profiles_to_rbac.sql
new file mode 100644
index 0000000..9c70b64
--- /dev/null
+++ b/assets/database/migration_profiles_to_rbac.sql
@@ -0,0 +1,222 @@
+-- ===================================================
+-- PROFILE TO RBAC MIGRATION SCRIPT
+-- Date: 2025-01-22
+-- Description: Migrate from settingsprofiles.php to user_roles RBAC system
+-- Note: Uses existing access_elements table (already populated)
+-- ===================================================
+
+START TRANSACTION;
+
+-- ===================================================
+-- PHASE 1: CREATE ROLES (matching existing profiles)
+-- ===================================================
+
+INSERT INTO `user_roles` (`name`, `description`, `is_active`, `created`, `createdby`) VALUES
+('Standard', 'Basic user access - view equipment, history, service reports', 1, NOW(), 1),
+('Superuser', 'Extended access - manage equipment, products, users', 1, NOW(), 1),
+('Admin', 'Administrative access - full management capabilities', 1, NOW(), 1),
+('AdminPlus', 'System administrator - complete system access', 1, NOW(), 1),
+('Build', 'Build tool access only', 1, NOW(), 1),
+('Commerce', 'E-commerce and catalog management', 1, NOW(), 1),
+('Distribution', 'Distribution partner access', 1, NOW(), 1),
+('Firmware', 'Firmware/software update access only', 1, NOW(), 1),
+('Garage', 'Car testing and diagnostics', 1, NOW(), 1),
+('Interface', 'API/Interface access', 1, NOW(), 1),
+('Service', 'Service technician access', 1, NOW(), 1),
+('Other', 'Miscellaneous access level', 1, NOW(), 1)
+ON DUPLICATE KEY UPDATE `description` = VALUES(`description`);
+
+-- ===================================================
+-- PHASE 2: CREATE ROLE_ACCESS_PERMISSIONS MAPPINGS
+-- ===================================================
+
+-- Get role IDs
+SET @role_standard = (SELECT rowID FROM user_roles WHERE name = 'Standard' LIMIT 1);
+SET @role_superuser = (SELECT rowID FROM user_roles WHERE name = 'Superuser' LIMIT 1);
+SET @role_admin = (SELECT rowID FROM user_roles WHERE name = 'Admin' LIMIT 1);
+SET @role_adminplus = (SELECT rowID FROM user_roles WHERE name = 'AdminPlus' LIMIT 1);
+SET @role_build = (SELECT rowID FROM user_roles WHERE name = 'Build' LIMIT 1);
+SET @role_commerce = (SELECT rowID FROM user_roles WHERE name = 'Commerce' LIMIT 1);
+SET @role_distribution = (SELECT rowID FROM user_roles WHERE name = 'Distribution' LIMIT 1);
+SET @role_firmware = (SELECT rowID FROM user_roles WHERE name = 'Firmware' LIMIT 1);
+SET @role_garage = (SELECT rowID FROM user_roles WHERE name = 'Garage' LIMIT 1);
+SET @role_interface = (SELECT rowID FROM user_roles WHERE name = 'Interface' LIMIT 1);
+SET @role_service = (SELECT rowID FROM user_roles WHERE name = 'Service' LIMIT 1);
+SET @role_other = (SELECT rowID FROM user_roles WHERE name = 'Other' LIMIT 1);
+
+-- ===================================================
+-- STANDARD ROLE PERMISSIONS (Read-only)
+-- Profile: application,firmwaretool,histories,history,servicereport,servicereports,dashboard,profile,equipment,equipments,products_software
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_standard, rowID, 0, 1, 0, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'firmwaretool', 'histories', 'history', 'servicereport', 'servicereports',
+ 'dashboard', 'profile', 'equipment', 'equipments', 'products_software'
+)
+ON DUPLICATE KEY UPDATE can_read = 1;
+
+-- ===================================================
+-- SUPERUSER ROLE PERMISSIONS (Create, Read, Update)
+-- Profile: application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,
+-- servicereport,servicereports,admin,dashboard,profile,equipment,equipment_manage,
+-- equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,
+-- products_software,products_versions,user,user_manage,users
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_superuser, rowID, 1, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'firmwaretool', 'histories', 'history', 'history_manage',
+ 'marketing', 'partner', 'partners', 'servicereport', 'servicereports',
+ 'dashboard', 'profile', 'equipment', 'equipment_manage',
+ 'equipments', 'equipments_mass_update', 'product', 'product_manage', 'products',
+ 'products_software', 'products_versions', 'user', 'users'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1;
+
+-- ===================================================
+-- ADMIN ROLE PERMISSIONS (Full CRUD)
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_admin, rowID, 1, 1, 1, 1 FROM access_elements WHERE access_path IN (
+ 'application', 'buildtool', 'cartest', 'cartest_manage', 'cartests',
+ 'changelog', 'communication', 'communication_send', 'communications', 'firmwaretool',
+ 'histories', 'history', 'history_manage', 'marketing', 'partner', 'partners',
+ 'servicereport', 'servicereports', 'software_available', 'software_download',
+ 'software_update', 'softwaretool', 'account', 'accounts', 'dashboard', 'profile',
+ 'contract', 'contract_manage', 'contracts', 'equipment', 'equipment_data',
+ 'equipment_healthindex', 'equipment_history', 'equipment_manage',
+ 'equipments', 'equipments_mass_update', 'product', 'product_manage', 'products',
+ 'products_software', 'products_software_assignment', 'products_software_assignments',
+ 'products_software_licenses', 'products_versions', 'report_build',
+ 'report_contracts_billing', 'report_healthindex', 'rma', 'rma_history',
+ 'rma_manage', 'rmas', 'user', 'users'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1, can_delete = 1;
+
+-- ===================================================
+-- ADMINPLUS ROLE PERMISSIONS (Full access to everything)
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_adminplus, rowID, 1, 1, 1, 1 FROM access_elements WHERE is_active = 1
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1, can_delete = 1;
+
+-- ===================================================
+-- BUILD ROLE PERMISSIONS
+-- Profile: application,buildtool,firmwaretool,dashboard,profile,products_software
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_build, rowID, 1, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'buildtool', 'firmwaretool', 'dashboard', 'profile', 'products_software'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1;
+
+-- ===================================================
+-- COMMERCE ROLE PERMISSIONS
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_commerce, rowID, 1, 1, 1, 1 FROM access_elements WHERE access_path IN (
+ 'application', 'catalog', 'categories', 'category', 'checkout', 'discount', 'discounts',
+ 'identity', 'invoice', 'media', 'media_manage', 'order', 'orders', 'partner', 'partners',
+ 'placeorder', 'pricelists', 'pricelists_items', 'pricelists_manage', 'shipping',
+ 'shipping_manage', 'shopping_cart', 'taxes', 'transactions', 'transactions_items',
+ 'translation_manage', 'translations', 'translations_details', 'uploader',
+ 'dashboard', 'profile', 'product', 'product_manage', 'products', 'products_attributes',
+ 'products_attributes_items', 'products_attributes_manage', 'products_categories',
+ 'products_configurations', 'products_media', 'products_software', 'products_versions',
+ 'user', 'users'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1, can_delete = 1;
+
+-- ===================================================
+-- DISTRIBUTION ROLE PERMISSIONS
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_distribution, rowID, 1, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'firmwaretool', 'histories', 'history', 'history_manage',
+ 'marketing', 'partner', 'partners', 'servicereport', 'servicereports',
+ 'dashboard', 'profile', 'equipment', 'equipment_manage',
+ 'equipments', 'equipments_mass_update', 'product', 'product_manage', 'products',
+ 'products_software', 'products_versions', 'user', 'users'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1;
+
+-- ===================================================
+-- FIRMWARE ROLE PERMISSIONS
+-- Profile: application,software_available,software_download,software_update,softwaretool,
+-- transactions,transactions_items,products_software_versions
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_firmware, rowID, 0, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'software_available', 'software_download', 'software_update',
+ 'softwaretool', 'transactions', 'transactions_items', 'products_software_versions'
+)
+ON DUPLICATE KEY UPDATE can_read = 1, can_update = 1;
+
+-- ===================================================
+-- GARAGE ROLE PERMISSIONS
+-- Profile: application,cartest,cartest_manage,cartests,dashboard,profile,products_versions
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_garage, rowID, 1, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'cartest', 'cartest_manage', 'cartests', 'dashboard', 'profile', 'products_versions'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1;
+
+-- ===================================================
+-- INTERFACE ROLE PERMISSIONS
+-- Profile: application,firmwaretool,invoice,payment,transactions,transactions_items,
+-- contract,contracts,equipment_manage,equipments,products_software,products_versions,users
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_interface, rowID, 1, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'firmwaretool', 'invoice', 'payment', 'transactions', 'transactions_items',
+ 'contract', 'contracts', 'equipment_manage', 'equipments', 'products_software',
+ 'products_versions', 'users'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1;
+
+-- ===================================================
+-- SERVICE ROLE PERMISSIONS
+-- Profile: application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,
+-- servicereport,servicereports,admin,dashboard,profile,equipment,equipment_manage,equipments,
+-- products_software,user,user_manage,users
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_service, rowID, 1, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'firmwaretool', 'histories', 'history', 'history_manage',
+ 'marketing', 'partner', 'partners', 'servicereport', 'servicereports',
+ 'dashboard', 'profile', 'equipment', 'equipment_manage', 'equipments', 'products_software',
+ 'user', 'users'
+)
+ON DUPLICATE KEY UPDATE can_create = 1, can_read = 1, can_update = 1;
+
+-- ===================================================
+-- OTHER ROLE PERMISSIONS
+-- Profile: application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,
+-- servicereport,servicereports,admin,dashboard,profile,equipment,equipment_manage,equipments,products_software
+-- ===================================================
+INSERT INTO `role_access_permissions` (`role_id`, `access_id`, `can_create`, `can_read`, `can_update`, `can_delete`)
+SELECT @role_other, rowID, 0, 1, 1, 0 FROM access_elements WHERE access_path IN (
+ 'application', 'firmwaretool', 'histories', 'history', 'history_manage',
+ 'marketing', 'partner', 'partners', 'servicereport', 'servicereports',
+ 'dashboard', 'profile', 'equipment', 'equipment_manage', 'equipments', 'products_software'
+)
+ON DUPLICATE KEY UPDATE can_read = 1, can_update = 1;
+
+-- ===================================================
+-- VERIFICATION QUERIES
+-- ===================================================
+
+-- Check roles created
+SELECT rowID, name, description, is_active FROM user_roles ORDER BY rowID;
+
+-- Check permissions per role
+SELECT ur.name as role_name, COUNT(rap.rowID) as permission_count
+FROM user_roles ur
+LEFT JOIN role_access_permissions rap ON ur.rowID = rap.role_id
+GROUP BY ur.rowID, ur.name
+ORDER BY ur.rowID;
+
+-- ===================================================
+-- Change ROLLBACK to COMMIT when ready to apply
+-- ===================================================
+COMMIT;
diff --git a/assets/database/migration_users_to_rbac.sql b/assets/database/migration_users_to_rbac.sql
new file mode 100644
index 0000000..3fc2400
--- /dev/null
+++ b/assets/database/migration_users_to_rbac.sql
@@ -0,0 +1,141 @@
+-- ===================================================
+-- USER TO RBAC ROLE ASSIGNMENT MIGRATION SCRIPT
+-- Date: 2025-01-22
+-- Description: Migrate users from settings/view fields to user_role_assignments
+-- Prerequisites: Run migration_profiles_to_rbac.sql first to create roles
+-- ===================================================
+
+START TRANSACTION;
+
+-- ===================================================
+-- MAPPING REFERENCE:
+--
+-- users.settings field values -> role names:
+-- 'admin_profile' or view=4 -> TSS_Admin
+-- 'distribution' -> Distribution
+-- 'service' -> Service
+-- 'firmware' -> Software_Tool
+-- 'interface' -> Interface
+-- 'superuser_profile' or view=1 -> Service
+-- All others (including empty/NULL) -> Service
+--
+-- IGNORED/REMOVED PROFILES:
+-- 'standard_profile', 'adminplus_profile', 'build', 'commerce',
+-- 'garage', 'other'
+-- ===================================================
+
+-- Get role IDs
+SET @role_tss_admin = (SELECT rowID FROM user_roles WHERE name = 'TSS_Admin' LIMIT 1);
+SET @role_distribution = (SELECT rowID FROM user_roles WHERE name = 'Distribution' LIMIT 1);
+SET @role_service = (SELECT rowID FROM user_roles WHERE name = 'Service' LIMIT 1);
+SET @role_software_tool = (SELECT rowID FROM user_roles WHERE name = 'Software_Tool' LIMIT 1);
+SET @role_interface = (SELECT rowID FROM user_roles WHERE name = 'Interface' LIMIT 1);
+
+-- ===================================================
+-- PHASE 1: MIGRATE USERS BY SETTINGS FIELD (profile name)
+-- ===================================================
+
+-- Users with 'admin_profile' setting -> TSS_Admin
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT id, @role_tss_admin, 1, 'migration_script', NOW(), NOW(), 1
+FROM users
+WHERE settings = 'admin_profile'
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- Users with 'distribution' setting -> Distribution
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT id, @role_distribution, 1, 'migration_script', NOW(), NOW(), 1
+FROM users
+WHERE settings = 'distribution'
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- Users with 'service' setting -> Service
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT id, @role_service, 1, 'migration_script', NOW(), NOW(), 1
+FROM users
+WHERE settings = 'service'
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- Users with 'firmware' setting -> Software_Tool
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT id, @role_software_tool, 1, 'migration_script', NOW(), NOW(), 1
+FROM users
+WHERE settings = 'firmware'
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- Users with 'interface' setting -> Interface
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT id, @role_interface, 1, 'migration_script', NOW(), NOW(), 1
+FROM users
+WHERE settings = 'interface'
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- Users with 'superuser_profile' setting -> Service
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT id, @role_service, 1, 'migration_script', NOW(), NOW(), 1
+FROM users
+WHERE settings = 'superuser_profile'
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- ===================================================
+-- PHASE 2: MIGRATE USERS WITH EMPTY/NULL SETTINGS (use view field)
+-- Only for users not already assigned a role
+-- ===================================================
+
+-- Users with view=4 (Admin) and no settings -> TSS_Admin
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT u.id, @role_tss_admin, 1, 'migration_script', NOW(), NOW(), 1
+FROM users u
+LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1
+WHERE (u.settings IS NULL OR u.settings = '')
+ AND u.view = '4'
+ AND ura.rowID IS NULL
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- ===================================================
+-- PHASE 3: CATCH-ALL - Any remaining users without role -> Service
+-- ===================================================
+
+INSERT INTO `user_role_assignments` (`user_id`, `role_id`, `is_active`, `assigned_by`, `assigned_at`, `created`, `createdby`)
+SELECT u.id, @role_service, 1, 'migration_script', NOW(), NOW(), 1
+FROM users u
+LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1
+WHERE ura.rowID IS NULL
+ON DUPLICATE KEY UPDATE updated = NOW();
+
+-- ===================================================
+-- VERIFICATION QUERIES
+-- ===================================================
+
+-- Check migration results: users per role
+SELECT
+ ur.name as role_name,
+ COUNT(ura.user_id) as user_count
+FROM user_roles ur
+LEFT JOIN user_role_assignments ura ON ur.rowID = ura.role_id AND ura.is_active = 1
+GROUP BY ur.rowID, ur.name
+ORDER BY user_count DESC;
+
+-- Check for users without role assignments (should be 0)
+SELECT COUNT(*) as users_without_role
+FROM users u
+LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1
+WHERE ura.rowID IS NULL;
+
+-- Compare old vs new: show users with their old settings and new role
+SELECT
+ u.id,
+ u.username,
+ u.settings as old_profile,
+ u.view as old_view_level,
+ ur.name as new_role
+FROM users u
+LEFT JOIN user_role_assignments ura ON u.id = ura.user_id AND ura.is_active = 1
+LEFT JOIN user_roles ur ON ura.role_id = ur.rowID
+ORDER BY u.id
+LIMIT 50;
+
+-- ===================================================
+-- Change ROLLBACK to COMMIT when ready to apply
+-- ===================================================
+COMMIT;
diff --git a/assets/database/user_rbac b/assets/database/user_rbac
new file mode 100644
index 0000000..12f9eb5
--- /dev/null
+++ b/assets/database/user_rbac
@@ -0,0 +1,65 @@
+CREATE TABLE `user_roles` (
+ `rowID` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) NOT NULL,
+ `description` text DEFAULT NULL,
+ `is_active` tinyint(1) NOT NULL DEFAULT 1,
+ `created` timestamp NULL DEFAULT current_timestamp(),
+ `createdby` int(11) DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ `updatedby` int(11) DEFAULT NULL,
+ PRIMARY KEY (`rowID`),
+ UNIQUE KEY `unique_role_name` (`name`)
+) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+CREATE TABLE `user_role_assignments` (
+ `rowID` int(11) NOT NULL AUTO_INCREMENT,
+ `user_id` int(11) NOT NULL,
+ `role_id` int(11) NOT NULL,
+ `is_active` tinyint(1) NOT NULL DEFAULT 1,
+ `assigned_by` varchar(255) DEFAULT NULL,
+ `assigned_at` timestamp NULL DEFAULT current_timestamp(),
+ `expires_at` timestamp NULL DEFAULT NULL,
+ `created` timestamp NULL DEFAULT current_timestamp(),
+ `createdby` int(11) DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ `updatedby` int(11) DEFAULT NULL,
+ PRIMARY KEY (`rowID`),
+ UNIQUE KEY `unique_user_role_active` (`user_id`,`role_id`,`is_active`),
+ KEY `role_id` (`role_id`),
+ CONSTRAINT `user_role_assignments_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`rowID`) ON DELETE CASCADE,
+ CONSTRAINT `user_role_assignments_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `user_roles` (`rowID`) ON DELETE CASCADE
+) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+REATE TABLE `role_access_permissions` (
+ `rowID` int(11) NOT NULL AUTO_INCREMENT,
+ `role_id` int(11) NOT NULL,
+ `access_id` int(11) NOT NULL,
+ `can_create` tinyint(1) NOT NULL DEFAULT 0,
+ `can_read` tinyint(1) NOT NULL DEFAULT 1,
+ `can_update` tinyint(1) NOT NULL DEFAULT 0,
+ `can_delete` tinyint(1) NOT NULL DEFAULT 0,
+ `created` timestamp NULL DEFAULT current_timestamp(),
+ `createdby` int(11) DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ `updatedby` int(11) DEFAULT NULL,
+ PRIMARY KEY (`rowID`),
+ UNIQUE KEY `unique_role_view` (`role_id`,`access_id`),
+ KEY `access_id` (`access_id`),
+ CONSTRAINT `role_view_permissions_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `user_roles` (`rowID`) ON DELETE CASCADE,
+ CONSTRAINT `role_view_permissions_ibfk_2` FOREIGN KEY (`access_id`) REFERENCES `system_views` (`rowID`) ON DELETE CASCADE
+) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+CREATE TABLE `access_elements` (
+ `rowID` int(11) NOT NULL AUTO_INCREMENT,
+ `access_group` varchar(100) NOT NULL,
+ `access_name` varchar(100) NOT NULL,
+ `access_path` varchar(255) NOT NULL,
+ `description` text DEFAULT NULL,
+ `is_active` tinyint(1) NOT NULL DEFAULT 1,
+ `created` timestamp NULL DEFAULT current_timestamp(),
+ `createdby` int(11) DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ `updatedby` int(11) DEFAULT NULL,
+ PRIMARY KEY (`rowID`),
+ UNIQUE KEY `unique_access_path` (`access_path`)
+) ENGINE=InnoDB AUTO_INCREMENT=393 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
\ No newline at end of file
diff --git a/assets/functions.php b/assets/functions.php
index 6e50226..27d009e 100644
--- a/assets/functions.php
+++ b/assets/functions.php
@@ -83,7 +83,9 @@ function send_mail($to, $subject, $message, $attachment, $attachment_name){
if( !$mail->send() ){
// render error if it is
$tab = array('error' => 'Mailer Error: '.$mail->ErrorInfo );
- debuglog(json_encode($tab));
+ if(debug){
+ debuglog(json_encode($tab));
+ }
exit;
}
else{
@@ -157,7 +159,9 @@ function sendIcsCalendar($appointment, $to, $subject = 'Appointment Confirmation
if (!$mail->send()) {
$tab = array('error' => 'Mailer Error: ' . $mail->ErrorInfo);
- debuglog(json_encode($tab));
+ if(debug){
+ debuglog(json_encode($tab));
+ }
return false;
} else {
return true;
@@ -229,20 +233,19 @@ function routes($urls) {
//------------------------------------------
// Menu Builder
//------------------------------------------
+/**
+ * @deprecated Use filterMenuByPermissions() instead
+ * Filter menu items based on profile string (legacy)
+ */
function filterMenuByProfile($menu, $profileString) {
- // Convert profile string to array
$profileArray = explode(',', $profileString);
-
- // Initialize result array
$filteredMenu = [];
-
- // Loop through main menu sections
+
foreach ($menu as $sectionKey => $section) {
$sectionIncluded = in_array($sectionKey, $profileArray);
$submenuFound = false;
$firstSubmenuItem = null;
-
- // First check if any submenu items are in profile
+
foreach ($section as $itemKey => $item) {
if ($itemKey !== 'main_menu' && in_array($itemKey, $profileArray)) {
$submenuFound = true;
@@ -251,24 +254,19 @@ function filterMenuByProfile($menu, $profileString) {
}
}
}
-
- // Include this section if either section key or any submenu is in profile
+
if ($sectionIncluded || $submenuFound) {
$filteredMenu[$sectionKey] = [];
-
- // Add main_menu - if section not in profile but submenu found, use first submenu as main_menu
+
if (!$sectionIncluded && $submenuFound && $firstSubmenuItem !== null) {
- // Create hybrid main_menu - keep name and icon from original, but use URL and selected from submenu
$hybridMainMenu = $section['main_menu'];
$hybridMainMenu['url'] = $firstSubmenuItem['url'];
$hybridMainMenu['selected'] = $firstSubmenuItem['selected'];
-
$filteredMenu[$sectionKey]['main_menu'] = $hybridMainMenu;
} else {
$filteredMenu[$sectionKey]['main_menu'] = $section['main_menu'];
}
-
- // Add allowed submenu items
+
foreach ($section as $itemKey => $item) {
if ($itemKey !== 'main_menu' && in_array($itemKey, $profileArray)) {
$filteredMenu[$sectionKey][$itemKey] = $item;
@@ -276,17 +274,83 @@ function filterMenuByProfile($menu, $profileString) {
}
}
}
-
+
return $filteredMenu;
}
-function menu($selected,$selected_child){
+
+/**
+ * Filter menu items based on user permissions array
+ *
+ * @param array $menu The full menu structure from settingsmenu.php
+ * @param array $permissions The permissions array from $_SESSION['authorization']['permissions']
+ * @return array Filtered menu with only items user has can_read permission for
+ */
+function filterMenuByPermissions($menu, $permissions) {
+ $filteredMenu = [];
+
+ foreach ($menu as $sectionKey => $section) {
+ // Get the main_menu's 'selected' path to check permission
+ $mainMenuPath = $section['main_menu']['selected'] ?? $sectionKey;
+
+ // Check if user has read permission for main menu
+ $mainMenuAllowed = isset($permissions[$mainMenuPath]) &&
+ $permissions[$mainMenuPath]['can_read'] == 1;
+
+ $allowedSubmenus = [];
+ $firstAllowedSubmenu = null;
+
+ // Check each submenu item for permission
+ foreach ($section as $itemKey => $item) {
+ if ($itemKey === 'main_menu') {
+ continue;
+ }
+
+ // Get the submenu item's 'selected' path
+ $submenuPath = $item['selected'] ?? $itemKey;
+
+ // Check if user has read permission for this submenu item
+ if (isset($permissions[$submenuPath]) &&
+ $permissions[$submenuPath]['can_read'] == 1) {
+ $allowedSubmenus[$itemKey] = $item;
+ if ($firstAllowedSubmenu === null) {
+ $firstAllowedSubmenu = $item;
+ }
+ }
+ }
+
+ // Include section if main menu is allowed OR any submenu is allowed
+ if ($mainMenuAllowed || count($allowedSubmenus) > 0) {
+ $filteredMenu[$sectionKey] = [];
+
+ // Handle main_menu entry
+ if (!$mainMenuAllowed && $firstAllowedSubmenu !== null) {
+ // User doesn't have main access but has submenu access
+ // Create hybrid: keep name/icon from main, use URL/selected from first submenu
+ $hybridMainMenu = $section['main_menu'];
+ $hybridMainMenu['url'] = $firstAllowedSubmenu['url'];
+ $hybridMainMenu['selected'] = $firstAllowedSubmenu['selected'];
+ $filteredMenu[$sectionKey]['main_menu'] = $hybridMainMenu;
+ } else {
+ $filteredMenu[$sectionKey]['main_menu'] = $section['main_menu'];
+ }
+
+ // Add allowed submenu items
+ foreach ($allowedSubmenus as $itemKey => $item) {
+ $filteredMenu[$sectionKey][$itemKey] = $item;
+ }
+ }
+ }
+
+ return $filteredMenu;
+}
+function menu($selected, $selected_child){
include dirname(__FILE__,2).'/settings/settings_redirector.php';
if(isset($_SESSION['country_code'])){
$api_file_language = dirname(__FILE__,2).'/settings/translations/translations_'.strtoupper($_SESSION['country_code']).'.php';
- if (file_exists($api_file_language)){
- include $api_file_language; //Include the code
+ if (file_exists($api_file_language)){
+ include $api_file_language;
}
else {
include dirname(__FILE__,2).'/settings/translations/translations_US.php';
@@ -294,31 +358,70 @@ function menu($selected,$selected_child){
}
else {
include dirname(__FILE__,2).'/settings/translations/translations_US.php';
- }
-
- //Define Menu
+ }
+
$menu = '';
- //filter the main_menu array based on profile
- $filteredMenu = filterMenuByProfile($main_menu, $_SESSION['profile']);
+ // Use permissions array if available, fallback to legacy profile string
+ if (isset($_SESSION['authorization']['permissions']) && !empty($_SESSION['authorization']['permissions'])) {
+ $filteredMenu = filterMenuByPermissions($main_menu, $_SESSION['authorization']['permissions']);
+ } else {
+ $filteredMenu = filterMenuByProfile($main_menu, $_SESSION['authorization']['profile']);
+ }
- foreach ($filteredMenu as $menu_item){
- //Main Item
- $menu .= '
'.ucfirst((${$menu_item['main_menu']['name']} ?? 'not specified')).'';
-
- if (count($menu_item) > 1){
- //SUBMENU
- $menu .= '
';
+ foreach ($filteredMenu as $menu_item) {
+ $submenuCount = count($menu_item) - 1; // Exclude main_menu
+ $mainMenu = $menu_item['main_menu'];
+ $menuName = ucfirst((${$mainMenu['name']} ?? ucfirst(str_replace('menu_', '', $mainMenu['name']))));
+ $isMainSelected = ($selected == $mainMenu['selected']);
- foreach ($menu_item as $key => $item){
- //filter out main_menu
- if($key !='main_menu'){
- $menu .= '
◼ '.ucfirst((${$item['name']}?? 'not specified')).'';
+ // Check if any child is selected (for expanded state)
+ $hasSelectedChild = false;
+ foreach ($menu_item as $key => $item) {
+ if ($key !== 'main_menu' && $selected == $item['selected']) {
+ $hasSelectedChild = true;
+ break;
}
- }
- $menu .= '
';
+ }
+
+ if ($submenuCount > 0) {
+ // HAS SUBMENUS: Render as collapsible header (not a link)
+ $expandedClass = ($isMainSelected || $hasSelectedChild) ? ' expanded' : '';
+ $selectedClass = $isMainSelected ? ' selected' : '';
+
+ $menu .= '
';
+ $menu .= ' ';
+ $menu .= '' . $menuName . ' ';
+ $menu .= '';
+ $menu .= '
';
+
+ // SUBMENU container
+ $subExpandedClass = ($isMainSelected || $hasSelectedChild) ? ' expanded' : '';
+ $subDisplayStyle = ($isMainSelected || $hasSelectedChild) ? 'display:flex;' : 'display:none;';
+ $menu .= '
';
+
+ foreach ($menu_item as $key => $item) {
+ if ($key !== 'main_menu') {
+ $itemName = ucfirst((${$item['name']} ?? ucfirst(str_replace('menu_', '', $item['name']))));
+ $itemSelectedClass = ($selected == $item['selected']) ? ' class="selected"' : '';
+ $menu .= '
';
+ $menu .= '◼ ';
+ $menu .= '' . $itemName . ' ';
+ $menu .= ' ';
+ }
+ }
+ $menu .= '
';
+ } else {
+ // NO SUBMENUS: Render as direct link
+ $selectedClass = $isMainSelected ? ' class="selected"' : '';
+ $menu .= '
';
+ $menu .= ' ';
+ $menu .= '' . $menuName . ' ';
+ $menu .= ' ';
+ $menu .= ' ';
}
}
+
return $menu;
}
@@ -336,12 +439,12 @@ function template_header($title, $selected = 'assets', $selected_child = 'view')
$domain = getDomainName($_SERVER['SERVER_NAME']);
$custom_css = (file_exists(dirname(__FILE__,2).'/custom/'.$domain.'/style/'.$domain.'.css') ? './custom/'.$domain.'/style/'.$domain.'.css' : './style/admin.css');
- $user = ucfirst($_SESSION['username']);
+ $user = ucfirst($_SESSION['authorization']['clientID']);
if (filter_var($user, FILTER_VALIDATE_EMAIL)){
$user = substr($user, 0, strpos($user, "@"));
}
- if (isset($_SESSION['id'])){$id = $_SESSION['id'];} else{$id='';}
+ if (isset($_SESSION['authorization']['id'])){$id = $_SESSION['authorization']['id'];} else{$id='';}
if(isset($_SESSION['country_code'])){
$api_file_language = dirname(__FILE__,2).'/settings/translations/translations_'.strtoupper($_SESSION['country_code']).'.php';
@@ -412,23 +515,19 @@ echo <<
{
- form.addEventListener('submit', function(e) {
- // Show loading screen before form submission
+ document.querySelectorAll('form').forEach(function(form) {
+ form.addEventListener('submit', function() {
showLoading();
});
});
}
-
+
// Intercept all network requests (fetch and XMLHttpRequest)
function interceptNetworkRequests() {
// Track active requests
@@ -516,8 +615,8 @@ EOT;
//------------------------------------------
function template_footer($js_script = '') {
$js_script = $js_script ? '' : '';
- $lancode = $_SESSION['language'] ?? 'US';
- $user_mail = $_SESSION['email'] ?? '';
+ $lancode = $_SESSION['authorization']['language'] ?? 'US';
+ $user_mail = $_SESSION['authorization']['email'] ?? '';
$veliti_cim = '';
if (veliti_cim){
$veliti_cim = '
@@ -1067,7 +1166,9 @@ function validate_secure_download_token($token, $secret_key = null) {
// Check JSON parsing with detailed error info
if ($header === null) {
$json_error = json_last_error_msg();
- debuglog("JSON decode failed for header. Raw JSON: " . $header_json . " Error: " . $json_error);
+ if(debug){
+ debuglog("JSON decode failed for header. Raw JSON: " . $header_json . " Error: " . $json_error);
+ }
return ['error' => 'INVALID_TOKEN', 'message' => 'Failed to decode token header JSON: ' . $json_error];
}
if ($payload === null) {
@@ -1227,8 +1328,13 @@ function log_download($params) {
function ioServer($api_call, $data){
include dirname(__FILE__,2).'/settings/settings_redirector.php';
+
+ if(debug){
+ $data_log = is_array($data) ? json_encode($data) : $data;
+ debuglog($date." - ioServer incoming call: api_call=$api_call, data=" . $data_log);
+ }
- $token = $_SESSION['userkey'] ?? 'authorization_request';
+ $token = $_SESSION['authorization']['userkey'] ?? 'authorization_request';
$bearertoken = createCommunicationToken($token);
$url = $baseurl.$api_call;
@@ -1252,7 +1358,13 @@ function ioServer($api_call, $data){
$resp = curl_exec($curl);
$http_status = curl_getinfo($curl) ?? '200';
curl_close($curl);
+
+ if (debug) {
+ $resp_log = $date . " - ioServer: URL=$url, HTTP Code= ". ($http_status['http_code'] ?? 'unknown') . ", Response=" . substr($resp, 0, 500) . (strlen($resp) > 500 ? '...' : '');
+ debuglog(json_encode($resp_log));
+ }
+
//Check If errorcode is returned
if($http_status['http_code'] == '403' || $http_status['http_code'] == '400') {$resp = generate_payload('NOK');}
@@ -1354,101 +1466,220 @@ function ioAPIv2($api_call, $data, $token){
}
//------------------------------------------
-// DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY ALL
+// API TO API version 2 File Upload
//------------------------------------------
+function ioAPIv2_FileUpload($api_call, $fileData, $additionalData = [], $token = '') {
+ include dirname(__FILE__,2).'/settings/settings_redirector.php';
+
+ $url = $baseurl . $api_call;
+
+ $curl = curl_init($url);
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($curl, CURLOPT_POST, true);
+
+ // Prepare headers (no Content-Type for multipart uploads)
+ if ($token != '') {
+ $headers = array("Authorization: Bearer $token");
+ } else {
+ $headers = array();
+ }
+ curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+
+ // Merge file data with additional data
+ $postData = array_merge($fileData, $additionalData);
+ curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
-function getWhereclause($table_name,$permission,$partner,$method){
+ $resp = curl_exec($curl);
+ $http_status = curl_getinfo($curl) ?? '200';
+ curl_close($curl);
- //api_name converter to table
- $table =[
- "equipment" => "e.accounthierarchy",
- "products" => "p.accounthierarchy",
- "profile" => "partnerhierarchy",
- "text_variables" => "tv.accounthierarchy",
- "products_attributes_items" => "pat.accounthierarchy",
- "products_attributes_groups" => "pag.accounthierarchy",
- "pricelists" => "pls.accounthierarchy",
- "pricelists_items" => "pli.accounthierarchy"
- ];
+ if ($http_status['http_code'] == '403' || $http_status['http_code'] == '400') {
+ $resp = json_encode('NOK');
+ }
+
+ if (debug){
+ $message = $date.';'.$api_call;
+ debuglog($message);
+ }
- $table = ($table_name != '') ? $table[$table_name] : 'accounthierarchy';
- $type = ($method == 'get') ? 'WHERE ' : ' AND ';
- //SoldTo is empty
- if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
-
- //default whereclause
- $whereclause = '';
-
- switch ($permission) {
- case '4':
- $whereclause = '';
- $condition = '';
- break;
- case '3':
- $condition = '__salesid___'.$partner->salesid.'___soldto___%';
- $whereclause = $type.$table.' like "'.$condition.'"';
- break;
- case '2':
- $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
- $whereclause = $type.$table.' like "'.$condition.'"';
- break;
- default:
- $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search.'___shipto___'.substr($partner->shipto, 0, strpos($partner->shipto, "-")).'%___location___'.substr($partner->location, 0, strpos($partner->location, "-")).'%';
- $whereclause = $type.$table.' like "'.$condition.'"';
- break;
- }
-
- return array($whereclause,$condition);
+ return $resp;
}
//------------------------------------------
-// DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY SALES AND SOLD
+// DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY
//------------------------------------------
-function getWhereclauselvl2($table_name,$permission,$partner,$method){
+function getWhereclause($table_name, $permission, $partner, $method) {
+ // API name converter to table
+ $table = [
+ "equipment" => "e.accounthierarchy",
+ "products" => "p.accounthierarchy",
+ "profile" => "partnerhierarchy",
+ "text_variables" => "tv.accounthierarchy",
+ "products_attributes_items" => "pat.accounthierarchy",
+ "products_attributes_groups" => "pag.accounthierarchy",
+ "pricelists" => "pls.accounthierarchy",
+ "pricelists_items" => "pli.accounthierarchy"
+ ];
- //api_name converter to table
- $table =[
- "pricelist" => "pls.accounthierarchy",
- "communications" => "salesID",
- "partners" => "salesID",
- "discounts" => "d.accounthierarchy",
- "invoice" => "inv.accounthierarchy",
- "attributes" => "pat.accounthierarchy",
- "config" => "pc.accounthierarchy",
- "software" => "p.accounthierarchy",
- "transactions" => "tx.accounthierarchy",
- "dealers" => "d.accounthierarchy",
- "categories" => "c.accounthierarchy",
- "products_software_licenses" => "l.accounthierarchy"
- ];
+ $table = ($table_name != '') ? $table[$table_name] : 'accounthierarchy';
+ $type = ($method == 'get') ? 'WHERE ' : ' AND ';
- $table = ($table_name != '') ? $table[$table_name] : 'accounthierarchy';
- $type = ($method == 'get') ? 'WHERE ' : ' AND ';
-
- //SoldTo is empty
- if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
+ // If permission is 4, grant full access (admin+)
+ if ($permission == '4' || $permission === 4) {
+ return array('', '');
+ }
- //default whereclause
- $whereclause = '';
+ // Determine hierarchy level based on which fields are filled
+ $hierarchy_level = getHierarchyLevel($partner);
+
+ // Build condition based on hierarchy level
+ $condition = buildHierarchyCondition($partner, $hierarchy_level);
+
+ // Build whereclause
+ $whereclause = ($condition != '') ? $type . $table . ' LIKE "' . $condition . '"' : '';
- switch ($permission) {
- case '4':
- $whereclause = '';
- $condition = '';
- break;
- case '3':
- $condition = '__salesid___'.$partner->salesid.'___soldto___%';
- $whereclause = $type.$table.' like "'.$condition.'" ';
- break;
- default:
- $condition = '__salesid___'.$partner->salesid.'___soldto___'.substr($partner->soldto, 0, strpos($partner->soldto, "-")).$soldto_search;
- $whereclause = $type.$table.' like "'.$condition.'"';
- break;
-}
-
- return array($whereclause,$condition);
+ return array($whereclause, $condition);
}
+function getWhereclauselvl2($table_name, $permission, $partner, $method) {
+ // API name converter to table
+ $table = [
+ "pricelist" => "pls.accounthierarchy",
+ "communications" => "salesID",
+ "partners" => "salesID",
+ "discounts" => "d.accounthierarchy",
+ "invoice" => "inv.accounthierarchy",
+ "attributes" => "pat.accounthierarchy",
+ "config" => "pc.accounthierarchy",
+ "software" => "p.accounthierarchy",
+ "transactions" => "tx.accounthierarchy",
+ "dealers" => "d.accounthierarchy",
+ "categories" => "c.accounthierarchy",
+ "products_software_licenses" => "l.accounthierarchy"
+ ];
+
+ $table = ($table_name != '') ? $table[$table_name] : 'accounthierarchy';
+ $type = ($method == 'get') ? 'WHERE ' : ' AND ';
+
+ // If permission is 4, grant full access (admin+)
+ if ($permission == '4' || $permission === 4) {
+ return array('', '');
+ }
+
+ // Determine hierarchy level (lvl2 only uses salesid and soldto)
+ $hierarchy_level = getHierarchyLevelLvl2($partner);
+
+ // Build condition based on hierarchy level
+ $condition = buildHierarchyConditionLvl2($partner, $hierarchy_level);
+
+ // Build whereclause
+ $whereclause = ($condition != '') ? $type . $table . ' LIKE "' . $condition . '"' : '';
+
+ return array($whereclause, $condition);
+}
+
+// Helper function to determine hierarchy level for full hierarchy (4 levels)
+function getHierarchyLevel($partner) {
+ // Level 4: All fields filled (salesid, soldto, shipto, location)
+ if (!empty($partner->salesid) && !empty($partner->soldto) &&
+ !empty($partner->shipto) && !empty($partner->location)) {
+ return 4;
+ }
+ // Level 3: salesid, soldto, shipto filled (location empty)
+ if (!empty($partner->salesid) && !empty($partner->soldto) &&
+ !empty($partner->shipto) && empty($partner->location)) {
+ return 3;
+ }
+ // Level 2: salesid, soldto filled (shipto and location empty)
+ if (!empty($partner->salesid) && !empty($partner->soldto) &&
+ empty($partner->shipto) && empty($partner->location)) {
+ return 2;
+ }
+ // Level 1: Only salesid filled
+ if (!empty($partner->salesid) && empty($partner->soldto)) {
+ return 1;
+ }
+ // Level 0: No restrictions (all access)
+ return 0;
+}
+
+// Helper function to determine hierarchy level for lvl2 (2 levels only)
+function getHierarchyLevelLvl2($partner) {
+ // Level 2: salesid and soldto filled
+ if (!empty($partner->salesid) && !empty($partner->soldto)) {
+ return 2;
+ }
+ // Level 1: Only salesid filled
+ if (!empty($partner->salesid) && empty($partner->soldto)) {
+ return 1;
+ }
+ // Level 0: No restrictions (all access)
+ return 0;
+}
+
+// Helper function to build condition string for full hierarchy
+function buildHierarchyCondition($partner, $level) {
+ $condition = '';
+
+ switch ($level) {
+ case 4: // Exact match on all 4 levels
+ $condition = '__salesid___' . $partner->salesid .
+ '___soldto___' . substr($partner->soldto, 0, strpos($partner->soldto, "-")) . '-' .
+ substr($partner->soldto, strpos($partner->soldto, "-") + 1) .
+ '___shipto___' . substr($partner->shipto, 0, strpos($partner->shipto, "-")) . '-' .
+ substr($partner->shipto, strpos($partner->shipto, "-") + 1) .
+ '___location___' . substr($partner->location, 0, strpos($partner->location, "-")) . '-' .
+ substr($partner->location, strpos($partner->location, "-") + 1) . '%';
+ break;
+
+ case 3: // Match salesid, soldto, shipto - all locations under this shipto
+ $condition = '__salesid___' . $partner->salesid .
+ '___soldto___' . substr($partner->soldto, 0, strpos($partner->soldto, "-")) . '-' .
+ substr($partner->soldto, strpos($partner->soldto, "-") + 1) .
+ '___shipto___' . substr($partner->shipto, 0, strpos($partner->shipto, "-")) . '-%';
+ break;
+
+ case 2: // Match salesid, soldto - all shiptos and locations under this soldto
+ $condition = '__salesid___' . $partner->salesid .
+ '___soldto___' . substr($partner->soldto, 0, strpos($partner->soldto, "-")) . '-%';
+ break;
+
+ case 1: // Match salesid only - all soldtos, shiptos, and locations under this salesid
+ $condition = '__salesid___' . $partner->salesid . '___soldto___%';
+ break;
+
+ case 0: // No restrictions
+ $condition = '';
+ break;
+ }
+
+ return $condition;
+}
+
+// Helper function to build condition string for lvl2
+function buildHierarchyConditionLvl2($partner, $level) {
+ $condition = '';
+
+ switch ($level) {
+ case 2: // Match salesid and soldto
+ $condition = '__salesid___' . $partner->salesid .
+ '___soldto___' . substr($partner->soldto, 0, strpos($partner->soldto, "-")) . '-%';
+ break;
+
+ case 1: // Match salesid only
+ $condition = '__salesid___' . $partner->salesid . '___soldto___%';
+ break;
+
+ case 0: // No restrictions
+ $condition = '';
+ break;
+ }
+
+ return $condition;
+}
+
+
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//get user profile||$profile=settings, $permision = userright()
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -1481,70 +1712,104 @@ function getProfile($profile, $permission){
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//Is allowed (yes=1)++++++++++++++++++++++++++++++++++++++++
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- function isAllowed($page,$profile,$permission,$action){
-
- //Include settingsa
- include dirname(__FILE__,2).'/settings/settings_redirector.php';
+ // RBAC-based permission check
+ // $access_element = the page/element to check access for (e.g., 'user', 'equipment')
+ // $permissions = array of user permissions from $_SESSION['authorization']['permissions'] (from getUserPermissions())
+ // $basic_permission_level = optional legacy permission level (5 = system, always allowed)
+ // $action = C, R, U, or D
+ function isAllowed($access_element, $permissions, $basic_permission_level = null, $action = 'R'){
+
+ $date = date('Y-m-d H:i:s');
+ $filelocation = dirname(__FILE__,2).'/log/permission_log_'.date('d').'.txt';
// Always allowed collections: [collection => allowed_actions_string]
$always_allowed = [
- 'com_log' => 'U',
+ 'com_log' => 'CRU',
+ 'application' => 'CRU',
+ 'user_role_assignments' => 'R',
+ 'user_permissions' => 'R',
+ 'products_software' => 'R',
'software_update' => 'R',
'software_download' => 'R',
- 'software_available' => 'R'
+ 'software_available' => 'R',
+ 'history' => 'RU',
+ 'payment' => 'RU'
];
- // Group permissions: [granting_page => [collection => allowed_actions_string]]
- $group_permissions = [
- 'products_software' => [
- 'products_software_version_access_rules' => 'CRU',
- 'products_software_licenses' => 'CRU',
- 'products_software_upgrade_paths' => 'CRU',
- 'products_software_versions' => 'CRU',
- 'products_software_assignment' => 'CRU',
- 'products_software_assignments' => 'CRU'
- ]
- ];
+ // 1. Check if basic_permission_level is 4 (System-admin+) - always allow
+ if ($basic_permission_level !== null && $basic_permission_level == 4) {
- // Debug log
- debuglog("isAllowed called: page=$page, permission=$permission, action=$action");
-
- // 1. Check always allowed
- if (isset($always_allowed[$page]) && str_contains($always_allowed[$page], $action)) {
- debuglog("Allowed by always_allowed");
return 1;
}
- //GET ALLOWED ACTIONS
- $user_permission = ${'permission_'.$permission};
-
- //CHECK ALLOWED
- $page_action = str_contains($user_permission,$action) > 0 ? 1 : 0; //CHECK IF USER IS ALLOWED TO DO THE ACTION
- $page_access = str_contains($profile,$page) > 0 ? 1 : 0; //CHECK USER IS ALLOWED TO ACCESS PAGE
-
- debuglog("user_permission=$user_permission, page_action=$page_action, page_access=$page_access");
-
- // 2. Check user permissions (standard)
- if ($page_access == 1 && $page_action == 1){
- debuglog("Allowed by user permissions");
- return 1;
- }
-
- // 3. If not allowed by user, check group permissions
- if ($page_access == 0) {
- foreach ($group_permissions as $granting_page => $grants) {
- if (str_contains($profile, $granting_page)) {
- debuglog("Found granting_page: $granting_page");
- if (isset($grants[$page]) && str_contains($grants[$page], $action)) {
- debuglog("Allowed by group permissions");
- return 1;
- }
+ // 2. Check always_allowed list (supports multi-action like 'RU')
+ if (isset($always_allowed[$access_element])) {
+ $actions = str_split($action);
+ $all_in_allowed = true;
+ foreach ($actions as $single_action) {
+ if (!str_contains($always_allowed[$access_element], $single_action)) {
+ $all_in_allowed = false;
+ break;
}
}
+ if ($all_in_allowed) {
+ return 1;
+ }
+ }
+
+ // 3. Check RBAC permissions array (from getUserPermissions())
+ if (is_array($permissions) && isset($permissions[$access_element])) {
+ $element_permissions = $permissions[$access_element];
+
+ // Map action letter to permission key
+ $action_map = [
+ 'C' => 'can_create',
+ 'R' => 'can_read',
+ 'U' => 'can_update',
+ 'D' => 'can_delete'
+ ];
+
+ // Check each action in the string (supports 'R', 'RU', 'CRUD', etc.)
+ $actions = str_split($action);
+ $all_allowed = true;
+
+ foreach ($actions as $single_action) {
+ $permission_key = $action_map[$single_action] ?? null;
+
+ if (!$permission_key || !isset($element_permissions[$permission_key]) || $element_permissions[$permission_key] != 1) {
+ $all_allowed = false;
+ break;
+ }
+ }
+
+ if ($all_allowed) {
+ return 1;
+ }
+
+ if(debug){
+ $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action".PHP_EOL;
+ error_log($test, 3, $filelocation);
+ $perm_value = $element_permissions[$permission_key] ?? 'not_set';
+ $test = "$date - RBAC check failed: $access_element -> $permission_key = $perm_value".PHP_EOL;
+ error_log($test, 3, $filelocation);
+ }
+ } else {
+ if(debug){
+ $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action".PHP_EOL;
+ error_log($test, 3, $filelocation);
+ $test = "$date - Access element '$access_element' not found in permissions array".PHP_EOL;
+ error_log($test, 3, $filelocation);
+ }
}
- debuglog("Not allowed");
// Not allowed
+ if(debug){
+ $test = "$date - isAllowed called: access_element=$access_element, basic_permission_level=$basic_permission_level, action=$action".PHP_EOL;
+ error_log($test, 3, $filelocation);
+ $test = "$date - Not allowed: access_element=$access_element, action=$action".PHP_EOL;
+ error_log($test, 3, $filelocation);
+ }
+
return 0;
}
@@ -1650,33 +1915,38 @@ function getPartnerID($str){
// overview Indicators
//------------------------------------------
function overviewIndicators($warranty, $service, $sw_version, $sw_version_latest){
- include dirname(__FILE__,2).'/settings/settings_redirector.php';
- include dirname(__FILE__,2).'/settings/systemfirmware.php';
- $indicator ='';
- //In warranty
- if (!empty($warranty ) && $warranty > $warrantydate){
- $indicator .= 'W ';
- } else {
- $indicator .= 'W ';
- }
- //Out of Service
- if (!empty($service) && $service < $servicedate){
- $indicator .= 'S ';
- } else {
- $indicator .= 'S ';
- }
+
+ include dirname(__FILE__,2).'/settings/settings_redirector.php';
+ include dirname(__FILE__,2).'/settings/systemfirmware.php';
+
+ $indicator ='';
+ $current_date = date('Y-m-d');
+
+ //In warranty
+ if (!empty($warranty ) && $warranty >= $current_date){
+ $indicator .= 'W ';
+ } else {
+ $indicator .= 'W ';
+ }
+ //Out of Service
+ if (!empty($service) && $service >= $current_date){
+ $indicator .= 'S ';
+ } else {
+ $indicator .= 'S ';
+ }
//Firmware
if (isset($sw_version_latest)){
- if($sw_version_latest == 1){
- $indicator .= 'F ';
+ if($sw_version_latest == 1){
+ $indicator .= 'F ';
+ }
+ else {
+ if ($sw_version == ''){
+ $indicator .= 'F ';
} else {
- if ($sw_version == ''){
- $indicator .= 'F ';
- } else {
- $indicator .= 'F ';
- }
+ $indicator .= 'F ';
}
+ }
}
return $indicator;
@@ -1693,23 +1963,24 @@ function warrantyStatus($input){
//INCLUDE TRANSLATION FILE
if(isset($_SESSION['country_code'])){
$api_file_language = dirname(__FILE__,2).'/settings/translations/translations_'.strtoupper($_SESSION['country_code']).'.php';
- if (file_exists($api_file_language)){
- include $api_file_language; //Include the code
- }
- else {
- include dirname(__FILE__,2).'/settings/translations/translations_US.php';
- }
+ if (file_exists($api_file_language)){
+ include $api_file_language; //Include the code
+ }
+ else {
+ include dirname(__FILE__,2).'/settings/translations/translations_US.php';
+ }
}
else {
include dirname(__FILE__,2).'/settings/translations/translations_US.php';
}
$warranty_date_due ='Unknown ';
-
- if (!empty($input) && $input < $warrantydate){
- $warranty_date_due = ''.$warranty_outdated_text.' ';
+ $current_date = date('Y-m-d');
+
+ if (!empty($input) && $input >= $current_date){
+ $warranty_date_due = ''.$warranty_recent.' ('.$input.') ';
} else {
- $warranty_date_due =''.$warranty_recent.' ('.date('Y-m-d', strtotime($input. ' + 365 days')).') ';
+ $warranty_date_due = ''.$warranty_outdated_text.' ';
}
return $warranty_date_due;
@@ -1736,13 +2007,15 @@ function serviceStatus($input){
else {
include dirname(__FILE__,2).'/settings/translations/translations_US.php';
}
-
+
+ $current_date = date('Y-m-d');
$service_date_due ='Unknown ';
- if (!empty($input) && $input < $servicedate){
- $service_date_due = ''.$service_renewal_text.' ';
+ if (!empty($input) && $input >= $current_date){
+ $service_date_due =''.$service_recent.' ('.$input.') ';
} else {
- $service_date_due =''.$service_recent.' ('.date('Y-m-d', strtotime($input. ' + 365 days')).') ';
+ $service_date_due = ''.$service_renewal_text.' ';
+
}
return $service_date_due;
@@ -1769,7 +2042,7 @@ function availableFirmware($sw_version,$sw_version_latest){
switch ($sw_version_latest) {
case 1:
- $message = ''.$firmware_recent_text.' ';
+ $message = ''.$firmware_recent_text.' ';
break;
case 0:
@@ -1777,7 +2050,7 @@ function availableFirmware($sw_version,$sw_version_latest){
break;
default:
- $message ='Unknown ';
+ $message ='✓ ';
break;
}
@@ -2581,7 +2854,7 @@ function listPartner($partnertype, $user_right, $input, $required)
//BASED ON USERRIGHT DEFINE SQL AND DATA RETURNED
if ($user_right != 3 || $user_right !=4) {
//NOT ADMIN USER
- $partner = json_decode($_SESSION['partnerhierarchy']);
+ $partner = json_decode($_SESSION['authorization']['partnerhierarchy']);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//BUILD CONDITION
@@ -2629,7 +2902,7 @@ function listAccounts($type, $user_right, $input)
//BASED ON USERRIGHT DEFINE SQL AND DATA RETURNED
if ($user_right != 3 || $user_right !=4) {
//NOT ADMIN USER
- $partner = json_decode($_SESSION['partnerhierarchy']);
+ $partner = json_decode($_SESSION['authorization']['partnerhierarchy']);
//SoldTo is empty
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
//BUILD CONDITION
@@ -2778,7 +3051,7 @@ function uploadrequest($key){
//------------------------------------------
function debuglog($error){
include_once dirname(__FILE__,2).'/settings/config_redirector.php';
- $test = $error.PHP_EOL;
+ $test = $error.PHP_EOL;
$filelocation = dirname(__FILE__,2).'/log/log_'.date('d').'.txt';
error_log($test, 3, $filelocation);
}
@@ -2898,20 +3171,29 @@ function showlog($object,$objectID){
$stmt->execute([$object,$objectID]);
$changes = $stmt->fetchAll(PDO::FETCH_ASSOC);
- $view = 'Changelog ';
- foreach($changes as $change){
-
- $object_value = $change['object_value'];
-
- //UPDATE TO HUMANREADABLE STATUS
- if ($object == 'equipment' && $change['object_field'] == 'status'){
- $object_text = 'status'.$change['object_value'].'_text';
- $object_value = $$object_text;
+ $view = '';
+ if ($changes) {
+ foreach ($changes as $change) {
+ $object_value = $change['object_value'];
+ // Human-readable status
+ if ($object == 'equipment' && $change['object_field'] == 'status') {
+ $object_text = 'status' . $change['object_value'] . '_text';
+ if (isset($$object_text)) {
+ $object_value = $$object_text;
+ }
+ }
+ $entry = htmlspecialchars( $object_value . ' - ' . $change['created'] . ' - ' . $change['createdby']);
+ $view .= '
+
'.$change['object_field'].'
+
'.$entry.'
+
';
+
+ }
+ } else {
+ $view .= '
No changelog entries found.
';
}
- $view .= '
';
- }
-
- return $view;
+ $view .= '
';
+ return $view;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -3726,27 +4008,29 @@ function dateInRange($start_date, $end_date, $date_check)
function getLatestVersion($productcode,$token){
- //CALL TO API TO GET ALL ACTIVE CONTRACTS
- $api_url = '/v2/products_software/productcode='.$productcode;
- $responses = ioAPIv2($api_url,'',$token);
+ //$pdo = dbConnect($dbname);
- //Decode Payload
- if (!empty($responses)){$responses = json_decode($responses,true);
- }
- else{
- $responses = $output = array(
- "productcode" => "",
- "version"=> "",
- "mandatory"=> "",
- "latest"=> "",
- "software"=> "",
- "source" => "",
- "source_type" => ""
- );
- ;}
+ //CALL TO API TO GET ALL ACTIVE CONTRACTS
+ $api_url = '/v2/products_software/productcode='.$productcode;
+ $responses = ioAPIv2($api_url,'',$token);
+
+ //Decode Payload
+ if (!empty($responses)){$responses = json_decode($responses,true);
+ }
+ else{
+ $responses = $output = array(
+ "productcode" => "",
+ "version"=> "",
+ "mandatory"=> "",
+ "latest"=> "",
+ "software"=> "",
+ "source" => "",
+ "source_type" => ""
+ );
+ ;}
- //DEFAULT OUTPUT
- return $responses;
+ //DEFAULT OUTPUT
+ return $responses;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -4374,6 +4658,7 @@ function transformOrderData(array $orderData): array {
'country' => $firstRow['address_country'],
'phone' => $firstRow['address_phone'],
'language' => $firstRow['user_language'],
+ 'vat_number' => $firstRow['vat_number']
],
'products' => [],
'invoice' => [
@@ -5163,7 +5448,7 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) {
SET e.sw_version_latest = 1
WHERE psv.latest = 1
AND psv.status = 1
- AND lower(e.sw_version) = lower(psv.version)
+ AND LOWER(TRIM(LEADING "0" FROM e.sw_version)) = lower(psv.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;
@@ -5339,10 +5624,12 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
$customer_country = $customer['address_country'] ?? '';
// Extract transaction data
- $payment_amount = $invoice_data['payment_amount'] ?? 0;
- $tax_amount = $invoice_data['tax_amount'] ?? 0;
- $shipping_amount = $invoice_data['shipping_amount'] ?? 0;
- $discount_amount = $invoice_data['discount_amount'] ?? 0;
+ $pricing = $invoice_data['pricing'] ?? [];
+ $payment_amount = $pricing['payment_amount'] ?? $invoice_data['payment_amount'] ?? 0;
+ $tax_amount = $pricing['tax_total'] ?? $invoice_data['tax_amount'] ?? 0;
+ $shipping_amount = $pricing['shipping_total'] ?? $invoice_data['shipping_amount'] ?? 0;
+ $discount_amount = $pricing['discount_total'] ?? $invoice_data['discount_amount'] ?? 0;
+ $subtotal_amount = $pricing['subtotal'] ?? 0;
$currency = 'EUR'; // Default currency
$invoice_date = $invoice_data['invoice_created'] ?? date('Y-m-d H:i:s');
@@ -5373,6 +5660,39 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
'serial_number' => $serial_number,
'license_key' => $license_key
];
+ } elseif (isset($invoice_data['products']) && is_array($invoice_data['products'])) {
+ // New format with products array
+ $pdo = dbConnect($dbname);
+
+ foreach ($invoice_data['products'] as $product) {
+ $product_code = $product['productcode'] ?? null;
+ $product_name = $product['product_name'] ?? null;
+ $product_options = $product['options'] ?? [];
+ $product_serial = $product_options['serial_number'] ?? null;
+
+ // Handle case where productcode and product_name are empty but serial_number exists
+ if ((empty($product_code) || $product_code === null) &&
+ (empty($product_name) || $product_name === null) &&
+ !empty($product_serial)) {
+ $product_code = 'License';
+ $product_name = 'software license for ' . $product_serial;
+ }
+
+ // Get license key from database
+ $sql = 'SELECT license_key FROM products_software_licenses WHERE transaction_id = ? LIMIT 1';
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$order_id]);
+ $license_result = $stmt->fetch(PDO::FETCH_ASSOC);
+ $license_key = $license_result['license_key'] ?? 'Pending';
+
+ $items[] = [
+ 'name' => $product_name ?? 'Software Upgrade',
+ 'quantity' => $product['quantity'] ?? 1,
+ 'price' => $product['price'] ?? 0,
+ 'serial_number' => $product_serial ?? 'N/A',
+ 'license_key' => $license_key
+ ];
+ }
}
// Load language translations
@@ -5402,131 +5722,23 @@ function generateSoftwareInvoice($invoice_data, $order_id, $language = 'US') {
$lbl_license_key = $translations['license_key'] ?? 'License Key';
$lbl_license_expiry = $translations['license_expiry'] ?? 'License Expiry';
- // Build HTML invoice
- $html = '
-
-
-
-
-
-
-
-
-
- ' . htmlspecialchars($lbl_customer) . ':
- ' . htmlspecialchars($customer_name) . ' ';
-
- if ($customer_address) {
- $html .= htmlspecialchars($customer_address) . ' ';
- }
- if ($customer_city || $customer_zip) {
- $html .= htmlspecialchars($customer_zip . ' ' . $customer_city) . ' ';
- }
- if ($customer_state) {
- $html .= htmlspecialchars($customer_state) . ' ';
- }
- if ($customer_country) {
- $html .= htmlspecialchars($customer_country) . ' ';
+ // Subtotal calculation - use from pricing data or calculate from items
+ if ($subtotal_amount > 0) {
+ $subtotal = $subtotal_amount;
+ } else {
+ // Calculate from items if not provided
+ $subtotal = 0;
+ foreach ($items as $item) {
+ $subtotal += $item['price'] * $item['quantity'];
+ }
}
- $html .= htmlspecialchars($customer_email) . '
-
+ // Build HTML for PDF and EMAIL
+ include dirname(__FILE__,2).'/assets/mail/email_template_invoice.php';
+ include dirname(__FILE__,2).'/assets/mail/pdf_template_invoice.php';
+
-
-
-
- ' . htmlspecialchars($lbl_product) . '
- ' . htmlspecialchars($lbl_quantity) . '
- ' . htmlspecialchars($lbl_price) . '
-
-
- ';
-
- foreach ($items as $item) {
- $html .= '
- ' . htmlspecialchars($item['name']) . '
- ' . htmlspecialchars($item['quantity']) . '
- ' . number_format($item['price'], 2) . ' ' . htmlspecialchars($currency) . '
- ';
- }
-
- // Subtotal
- $subtotal = $payment_amount - $tax_amount - $shipping_amount + $discount_amount;
- $html .= '
- ' . htmlspecialchars($lbl_subtotal) . ':
- ' . number_format($subtotal, 2) . ' ' . htmlspecialchars($currency) . '
- ';
-
- // Tax
- if ($tax_amount > 0) {
- $html .= '
- ' . htmlspecialchars($lbl_tax) . ':
- ' . number_format($tax_amount, 2) . ' ' . htmlspecialchars($currency) . '
- ';
- }
-
- // Shipping
- if ($shipping_amount > 0) {
- $html .= '
- ' . htmlspecialchars($lbl_shipping) . ':
- ' . number_format($shipping_amount, 2) . ' ' . htmlspecialchars($currency) . '
- ';
- }
-
- // Discount
- if ($discount_amount > 0) {
- $html .= '
- ' . htmlspecialchars($lbl_discount) . ':
- -' . number_format($discount_amount, 2) . ' ' . htmlspecialchars($currency) . '
- ';
- }
-
- // Total
- $html .= '
- ' . htmlspecialchars($lbl_total) . ':
- ' . number_format($payment_amount, 2) . ' ' . htmlspecialchars($currency) . '
- ';
-
- $html .= '
-
';
-
- // License information
- if ($license_key && $serial_number) {
- $html .= '
- Software License Information:
- ' . htmlspecialchars($lbl_device_serial) . ': ' . htmlspecialchars($serial_number) . '
- ' . htmlspecialchars($lbl_license_key) . ': ' . htmlspecialchars($license_key) . '
- ' . htmlspecialchars($lbl_license_expiry) . ': 2099-12-31
-
';
- }
-
- $html .= '
-
-';
-
- return [$html, $customer_email, $order_id];
+ return [$message,$pdf,$customer_email, $order_id];
}
/**
@@ -5572,4 +5784,121 @@ function updateSoftwareLatestFlags($pdo, $version_id, $hw_version) {
$stmt->execute([$version['rowID']]);
}
}
+}
+
+// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+// Generate Countries File from Taxes API +++++++++++++++++
+// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
+function generateCountriesFile($token){
+
+ //API call to get all taxes
+ $api_url = '/v2/taxes';
+ $response = ioAPIv2($api_url, '', $token);
+
+ if(!empty($response)){
+ //decode the API response
+ $taxes = json_decode($response, true);
+
+ if(!empty($taxes) && is_array($taxes)){
+ //Build the countries array - id as key, with country name and tax rate
+ $countries = [];
+ foreach($taxes as $tax){
+ $countries[$tax['id']] = [
+ 'country' => $tax['country'] ?? '',
+ 'taxes' => $tax['rate'] ?? 0
+ ];
+ }
+
+ //Generate PHP file content
+ $fileContent = " $data){
+ $fileContent .= " " . $id . " => ['country' => '" . addslashes($data['country']) . "', 'taxes' => " . $data['taxes'] . "],\n";
+ }
+ $fileContent .= "];\n";
+
+ //Write to settings/countries.php
+ $filePath = dirname(__FILE__, 2) . '/settings/countries.php';
+ $result = file_put_contents($filePath, $fileContent);
+
+ return ($result !== false);
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Get combined user permissions based on all assigned roles
+ *
+ * This function retrieves all role assignments for a user and combines permissions
+ * from multiple roles. If the same access_element appears in multiple roles,
+ * permissions are merged (OR operation) so the user gets the union of all permissions.
+ *
+ * For example:
+ * - Role A: access_element 'assets' with C=1, U=1, D=0
+ * - Role B: access_element 'assets' with C=0, U=0, D=1
+ * - Result: access_element 'assets' with C=1, U=1, D=1
+ *
+ * @param PDO $pdo Database connection
+ * @param int $user_id The user ID to get permissions for
+ * @return array Associative array of permissions indexed by access_element path
+ * Each element contains: [path, name, group, can_create, can_read, can_update, can_delete]
+ */
+function getUserPermissions($pdo, $user_id) {
+ // Get all active role assignments for the user with their permissions
+ $sql = "SELECT
+ ae.access_path,
+ ae.access_name,
+ ae.access_group,
+ rap.can_create,
+ rap.can_read,
+ rap.can_update,
+ rap.can_delete
+ FROM user_role_assignments ura
+ INNER JOIN user_roles ur ON ura.role_id = ur.rowID
+ INNER JOIN role_access_permissions rap ON ur.rowID = rap.role_id
+ INNER JOIN access_elements ae ON rap.access_id = ae.rowID
+ WHERE ura.user_id = :user_id
+ AND ura.is_active = 1
+ AND ur.is_active = 1
+ AND ae.is_active = 1
+ AND (ura.expires_at IS NULL OR ura.expires_at > NOW())
+ ORDER BY ae.access_path";
+
+ $stmt = $pdo->prepare($sql);
+ $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
+ $stmt->execute();
+ $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ // Combine permissions for duplicate access elements
+ $combined_permissions = [];
+
+ foreach ($results as $row) {
+ $path = $row['access_path'];
+
+ if (!isset($combined_permissions[$path])) {
+ // First time seeing this access element
+ $combined_permissions[$path] = [
+ 'path' => $row['access_path'],
+ 'name' => $row['access_name'],
+ 'group' => $row['access_group'],
+ 'can_create' => (int)$row['can_create'],
+ 'can_read' => (int)$row['can_read'],
+ 'can_update' => (int)$row['can_update'],
+ 'can_delete' => (int)$row['can_delete']
+ ];
+ } else {
+ // Access element already exists, combine permissions (OR operation)
+ // If any role grants a permission, the user has that permission
+ $combined_permissions[$path]['can_create'] = max($combined_permissions[$path]['can_create'], (int)$row['can_create']);
+ $combined_permissions[$path]['can_read'] = max($combined_permissions[$path]['can_read'], (int)$row['can_read']);
+ $combined_permissions[$path]['can_update'] = max($combined_permissions[$path]['can_update'], (int)$row['can_update']);
+ $combined_permissions[$path]['can_delete'] = max($combined_permissions[$path]['can_delete'], (int)$row['can_delete']);
+ }
+ }
+
+ return $combined_permissions;
}
\ No newline at end of file
diff --git a/assets/images/TSS_invoice_footer.png b/assets/images/TSS_invoice_footer.png
new file mode 100644
index 0000000..c6129f5
Binary files /dev/null and b/assets/images/TSS_invoice_footer.png differ
diff --git a/assets/images/TSS_invoice_header.png b/assets/images/TSS_invoice_header.png
new file mode 100644
index 0000000..aa6ef31
Binary files /dev/null and b/assets/images/TSS_invoice_header.png differ
diff --git a/assets/mail/email_template_invoice.php b/assets/mail/email_template_invoice.php
new file mode 100644
index 0000000..429edea
--- /dev/null
+++ b/assets/mail/email_template_invoice.php
@@ -0,0 +1,164 @@
+
+
+
+
+
+ ' . htmlspecialchars($lbl_invoice) . ' - Total Safety Solutions
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . htmlspecialchars($lbl_invoice) . '
+
+
+
+
+
+ Total Safety Solutions B.V.
+ Laarakkerweg 8
+ 5061 JR OISTERWIJK
+ Nederland
+
+
+ contact-details
+ Ralf Adams
+ +31 13 8221480
+ ralfadams@totalsafetysolutions.nl
+
+
+
+
+
+
+
+
+ Customer
+ '.$invoice_data['customer']['name'].'
+ '.$invoice_data['customer']['street'].'
+ '.$invoice_data['customer']['zip'].', '.$invoice_data['customer']['city'].'
+ '.$invoice_data['customer']['country'].'
+
+
+
+
+
+
+
+
+
+
+
+ Invoice Date:
+ ' . htmlspecialchars(date('d-m-Y', strtotime($invoice_date))) . '
+
+
+ Invoice Number:
+ ' . htmlspecialchars($order_id) . '
+
+
+ Your Vat Number:
+ ' . htmlspecialchars($invoice_data['customer']['vat_number'] ?? '') . '
+
+
+
+
+
+
+ Reference:
+ Online order
+
+
+ Order number:
+ ' . htmlspecialchars($order_id) . '
+
+
+ Payment Methodr:
+ ' . (${$payment_method} ?? $invoice_data['header']['payment_method'] ). '
+
+
+
+
+
+
+
+
+
+
+ Item code
+ Description
+ Quantity
+ Price
+ Total
+
+
+ ';
+
+foreach ($items as $item) {
+ $line_total = $item['price'] * $item['quantity'];
+ $message .= '
+ SOFTWARE
+ ' . htmlspecialchars($item['name']);
+
+ if ($item['serial_number'] !== 'N/A') {
+ $message .= 'Serial: ' . htmlspecialchars($item['serial_number']) . ' ';
+ }
+ if ($item['license_key'] !== 'Pending') {
+ $message .= 'License: ' . htmlspecialchars($item['license_key']) . ' ';
+ }
+
+ $message .= '
+ ' . htmlspecialchars($item['quantity']) . '
+ € ' . number_format($item['price'], 2) . '
+ € ' . number_format($line_total, 2) . '
+ ';
+}
+
+$message .= '
+
+
+
+
+
+ ' . htmlspecialchars($lbl_subtotal) . '
+ € ' . number_format($subtotal, 2) . '
+ ';
+
+if ($tax_amount > 0) {
+ $message .= '
+ ' . htmlspecialchars($lbl_tax) . '
+ € ' . number_format($tax_amount, 2) . '
+ ';
+} else {
+ $message .= '
+ VAT
+ included
+ ';
+}
+
+$message .= '
+ ' . htmlspecialchars($lbl_total) . '
+ € ' . number_format($payment_amount, 2) . '
+
+
+
+
+
+
+
+
+
+
+
+';
\ No newline at end of file
diff --git a/assets/mail/pdf_template_invoice.php b/assets/mail/pdf_template_invoice.php
new file mode 100644
index 0000000..29aea68
--- /dev/null
+++ b/assets/mail/pdf_template_invoice.php
@@ -0,0 +1,329 @@
+
+
+
+
+
+ ' . htmlspecialchars($lbl_invoice) . ' - Total Safety Solutions
+
+
+
+
+
+
+ ' . htmlspecialchars($lbl_invoice) . '
+
+
+
+
+
+
+
+
+
Invoice Date
+
: ' . htmlspecialchars(date('d-m-Y', strtotime($invoice_date))) . '
+
+
+
Invoice Number
+
: ' . htmlspecialchars($order_id) . '
+
+
+
Your Vat Number
+
: ' . htmlspecialchars($invoice_data['customer']['vat_number'] ?? '') . '
+
+
+
+
+
Reference
+
: Online order
+
+
+
Order number
+
: ' . htmlspecialchars($order_id) . '
+
+
+
Payment Method
+
: ' . (${$payment_method} ?? $invoice_data['header']['payment_method'] ). '
+
+
+
+
+
+
+
+ Item code
+ Description
+ Quantity
+ Price
+ Total
+
+
+ ';
+
+ foreach ($items as $item) {
+ $line_total = $item['price'] * $item['quantity'];
+ $pdf .= '
+ SOFTWARE
+ ' . htmlspecialchars($item['name']);
+
+ if ($item['serial_number'] !== 'N/A') {
+ $pdf .= 'Serial: ' . htmlspecialchars($item['serial_number']) . ' ';
+ }
+ if ($item['license_key'] !== 'Pending') {
+ $pdf .= 'License: ' . htmlspecialchars($item['license_key']) . ' ';
+ }
+
+ $pdf .= '
+ ' . htmlspecialchars($item['quantity']) . '
+ € ' . number_format($item['price'], 2) . '
+ € ' . number_format($line_total, 2) . '
+ ';
+ }
+
+$pdf .= '
+
+
+
+
+
' . htmlspecialchars($lbl_subtotal) . '
+
€ ' . number_format($subtotal, 2) . '
+
';
+
+ if ($tax_amount > 0) {
+ $pdf .= '
+
' . htmlspecialchars($lbl_tax) . '
+
€ ' . number_format($tax_amount, 2) . '
+
';
+ } else {
+ $pdf .= '
';
+ }
+
+ $pdf .= '
+
' . htmlspecialchars($lbl_total) . '
+
€ ' . number_format($payment_amount, 2) . '
+
+
+
+
+';
\ No newline at end of file
diff --git a/assets/marketing.js b/assets/marketing.js
new file mode 100644
index 0000000..240287f
--- /dev/null
+++ b/assets/marketing.js
@@ -0,0 +1,1354 @@
+/**
+ * Marketing File Management System
+ * Professional drag-and-drop upload with folder management and tagging
+ */
+
+class MarketingFileManager {
+ constructor() {
+ this.currentFolder = '';
+ this.selectedFiles = [];
+ this.uploadQueue = [];
+ this.viewMode = 'grid';
+ this.filters = {
+ search: '',
+ tag: '',
+ fileTypes: []
+ };
+ this.folders = []; // Store folders data
+ this.loadRequestId = 0; // Track the latest load request
+
+ // Get permissions from PHP
+ this.permissions = window.marketingPermissions || {
+ canCreate: 0,
+ canUpdate: 0,
+ canDelete: 0
+ };
+
+ this.init();
+ }
+
+ init() {
+ this.bindEvents();
+ this.loadFolders();
+ this.loadTags();
+ this.loadFiles();
+ this.setupDragAndDrop();
+ }
+
+ bindEvents() {
+ // Upload modal
+ document.getElementById('uploadBtn')?.addEventListener('click', () => {
+ this.showUploadModal();
+ });
+
+ // Create folder modal
+ document.getElementById('createFolderBtn')?.addEventListener('click', () => {
+ this.showFolderModal();
+ });
+
+ // View mode toggle
+ document.getElementById('gridViewBtn')?.addEventListener('click', () => {
+ this.setViewMode('grid');
+ });
+
+ document.getElementById('listViewBtn')?.addEventListener('click', () => {
+ this.setViewMode('list');
+ });
+
+ // Search
+ document.getElementById('searchInput')?.addEventListener('input', (e) => {
+ this.filters.search = e.target.value;
+ this.debounce(this.loadFiles.bind(this), 300)();
+ });
+
+ // Tag filter
+ document.getElementById('tagFilter')?.addEventListener('change', (e) => {
+ this.filters.tag = e.target.value;
+ this.loadFiles();
+ });
+
+ // File type filters
+ document.querySelectorAll('.file-type-filters input[type="checkbox"]').forEach(checkbox => {
+ checkbox.addEventListener('change', () => {
+ this.updateFileTypeFilters();
+ });
+ });
+
+ // Modal events
+ this.bindModalEvents();
+
+ // Upload events
+ this.bindUploadEvents();
+ }
+
+ bindModalEvents() {
+ // Close modals
+ document.querySelectorAll('.modal-close, .modal-cancel').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ this.closeModal(e.target.closest('.modal'));
+ });
+ });
+
+ // Create folder
+ document.getElementById('createFolder')?.addEventListener('click', () => {
+ this.createFolder();
+ });
+
+ // Download file
+ document.getElementById('downloadFile')?.addEventListener('click', () => {
+ if (this.selectedFile) {
+ this.downloadFile(this.selectedFile);
+ }
+ });
+
+ // Delete file
+ document.getElementById('deleteFile')?.addEventListener('click', () => {
+ if (this.selectedFile) {
+ this.deleteFile(this.selectedFile);
+ }
+ });
+
+ // Save edit
+ document.getElementById('saveEdit')?.addEventListener('click', () => {
+ this.saveEdit();
+ });
+
+ // Edit folder
+ document.getElementById('saveEditFolder')?.addEventListener('click', () => {
+ this.saveEditFolder();
+ });
+
+ // Delete folder
+ document.getElementById('deleteFolder')?.addEventListener('click', () => {
+ if (this.selectedFolder) {
+ this.deleteFolder(this.selectedFolder);
+ }
+ });
+ }
+
+ bindUploadEvents() {
+ const fileInput = document.getElementById('fileInput');
+ const browseBtn = document.getElementById('browseBtn');
+ const startUpload = document.getElementById('startUpload');
+
+ browseBtn?.addEventListener('click', () => {
+ fileInput.click();
+ });
+
+ fileInput?.addEventListener('change', (e) => {
+ this.handleFileSelect(e.target.files);
+ });
+
+ startUpload?.addEventListener('click', () => {
+ this.startUpload();
+ });
+ }
+
+ setupDragAndDrop() {
+ const uploadArea = document.getElementById('uploadArea');
+ const filesContainer = document.getElementById('filesContainer');
+
+ if (uploadArea) {
+ uploadArea.addEventListener('dragover', this.handleDragOver);
+ uploadArea.addEventListener('drop', (e) => this.handleDrop(e));
+ }
+
+ if (filesContainer) {
+ filesContainer.addEventListener('dragover', this.handleDragOver);
+ filesContainer.addEventListener('drop', (e) => this.handleDrop(e));
+ }
+ }
+
+ handleDragOver(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.currentTarget.classList.add('drag-over');
+ }
+
+ handleDrop(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.currentTarget.classList.remove('drag-over');
+
+ const files = e.dataTransfer.files;
+ if (files.length > 0) {
+ this.showUploadModal();
+ this.handleFileSelect(files);
+ }
+ }
+
+ async loadFolders() {
+ try {
+ const response = await fetch('index.php?page=marketing&action=marketing_folders&tree=true', { cache: 'no-store' });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+ if (!text || text.trim() === '') {
+ console.warn('Empty response from folders API');
+ this.folders = [];
+ this.renderFolderTree([]);
+ this.populateFolderSelects([]);
+ return;
+ }
+
+ const data = JSON.parse(text);
+
+ this.folders = data || []; // Store the folders data
+ // Always render the folder tree (at minimum shows Root)
+ this.renderFolderTree(this.folders);
+ this.populateFolderSelects(this.folders);
+ } catch (error) {
+ console.error('Error loading folders:', error);
+ this.folders = [];
+ // Show at least root folder on error
+ this.renderFolderTree([]);
+ this.populateFolderSelects([]);
+ }
+ }
+
+ async loadTags() {
+ try {
+ const response = await fetch('index.php?page=marketing&action=marketing_tags&used_only=true', { cache: 'no-store' });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+ if (!text || text.trim() === '') {
+ console.warn('Empty response from tags API');
+ this.populateTagFilter([]);
+ return;
+ }
+
+ const data = JSON.parse(text);
+
+ // Always populate tag filter (at minimum shows "All Tags")
+ this.populateTagFilter(data || []);
+ } catch (error) {
+ console.error('Error loading tags:', error);
+ // Show empty tag filter on error
+ this.populateTagFilter([]);
+ }
+ }
+
+ async loadFiles() {
+ const container = document.getElementById('filesContainer');
+ const loading = document.getElementById('loadingIndicator');
+ const emptyState = document.getElementById('emptyState');
+
+ // Increment request ID to invalidate previous requests
+ const requestId = ++this.loadRequestId;
+
+ // Clear container FIRST to prevent showing old files
+ container.innerHTML = '';
+ loading.style.display = 'block';
+ emptyState.style.display = 'none';
+
+ try {
+ // Add cache busting to prevent browser caching
+ let url = `index.php?page=marketing&action=marketing_files&limit=50&_t=${Date.now()}`;
+
+ // Only filter by folder if no tag filter is active (tag search is across all folders)
+ if (!this.filters.tag) {
+ const folderId = this.currentFolder ? this.currentFolder : 'null';
+ url += `&folder_id=${folderId}`;
+ }
+
+ if (this.filters.search) {
+ url += `&search=${encodeURIComponent(this.filters.search)}`;
+ }
+
+ if (this.filters.tag) {
+ url += `&tag=${encodeURIComponent(this.filters.tag)}`;
+ }
+
+ if (this.filters.fileTypes.length > 0) {
+ // API expects individual file_type parameter, so we'll filter client-side for now
+ }
+
+ const response = await fetch(url, { cache: 'no-store' });
+
+ // Ignore response if a newer request was made
+ if (requestId !== this.loadRequestId) {
+ return;
+ }
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+
+ if (!text || text.trim() === '') {
+ console.warn('Empty response from files API');
+ emptyState.style.display = 'block';
+ return;
+ }
+
+ const data = JSON.parse(text);
+
+ if (data && data.length > 0) {
+ let files = data;
+
+ // Client-side file type filtering
+ if (this.filters.fileTypes.length > 0) {
+ files = files.filter(file =>
+ this.filters.fileTypes.includes(file.file_type.toLowerCase())
+ );
+ }
+
+ if (files.length === 0) {
+ // No files after filtering, check for subfolders
+ const subfolders = this.getSubfolders(this.currentFolder);
+ if (subfolders.length > 0) {
+ this.renderFolderTiles(subfolders);
+ } else {
+ emptyState.style.display = 'block';
+ }
+ } else {
+ this.renderFiles(files);
+ }
+ } else {
+ // No files, check for subfolders
+ const subfolders = this.getSubfolders(this.currentFolder);
+ if (subfolders.length > 0) {
+ this.renderFolderTiles(subfolders);
+ } else {
+ emptyState.style.display = 'block';
+ }
+ }
+ } catch (error) {
+ console.error('Error loading files:', error);
+ this.showToast('Error loading files', 'error');
+ } finally {
+ loading.style.display = 'none';
+ }
+ }
+
+ renderFolderTree(folders, container = null, level = 0) {
+ if (!container) {
+ container = document.getElementById('folderTree');
+ container.innerHTML = ' Root
';
+
+ // Add click listener to root folder
+ const rootFolder = container.querySelector('.folder-item.root');
+ if (rootFolder) {
+ rootFolder.addEventListener('click', () => {
+ this.selectFolder('');
+ });
+ }
+ }
+
+ folders.forEach(folder => {
+ const folderItem = document.createElement('div');
+ folderItem.className = 'folder-item';
+ folderItem.setAttribute('data-folder', folder.id);
+ folderItem.style.marginLeft = `${level * 20}px`;
+
+ const hasChildren = folder.children && folder.children.length > 0;
+ const expandIcon = hasChildren ? ' ' : '';
+
+ // Only show edit button if user has update permission
+ const editButton = this.permissions.canUpdate === 1
+ ? `
+
+ `
+ : '';
+
+ folderItem.innerHTML = `
+ ${expandIcon}
+
+ ${this.escapeHtml(folder.folder_name)}
+ (${folder.file_count})
+ ${editButton}
+ `;
+
+ folderItem.addEventListener('click', (e) => {
+ // Don't select folder if edit button was clicked
+ if (e.target.closest('.folder-edit-btn')) {
+ e.stopPropagation();
+ this.editFolder(folder);
+ } else {
+ this.selectFolder(folder.id);
+ }
+ });
+
+ container.appendChild(folderItem);
+
+ if (hasChildren) {
+ this.renderFolderTree(folder.children, container, level + 1);
+ }
+ });
+ }
+
+ renderFiles(files) {
+ const container = document.getElementById('filesContainer');
+ container.innerHTML = '';
+
+ files.forEach(file => {
+ const fileElement = this.createFileElement(file);
+ container.appendChild(fileElement);
+ });
+ }
+
+ getSubfolders(folderId) {
+ // Find immediate children of the specified folder
+ if (!folderId || folderId === '') {
+ // Root folder - return top-level folders
+ return this.folders;
+ }
+
+ // Recursively search for the folder and return its children
+ const findFolder = (folders, targetId) => {
+ for (const folder of folders) {
+ if (folder.id === targetId) {
+ return folder.children || [];
+ }
+ if (folder.children && folder.children.length > 0) {
+ const found = findFolder(folder.children, targetId);
+ if (found) return found;
+ }
+ }
+ return [];
+ };
+
+ return findFolder(this.folders, folderId);
+ }
+
+ renderFolderTiles(subfolders) {
+ const container = document.getElementById('filesContainer');
+ container.innerHTML = '';
+
+ subfolders.forEach(folder => {
+ const folderElement = this.createFolderTileElement(folder);
+ container.appendChild(folderElement);
+ });
+ }
+
+ createFolderTileElement(folder) {
+ const folderElement = document.createElement('div');
+ folderElement.className = `folder-tile ${this.viewMode}-item`;
+ folderElement.setAttribute('data-folder-id', folder.id);
+
+ folderElement.innerHTML = `
+
+
+
+
+
+ ${this.escapeHtml(folder.folder_name)}
+
+
+ ${folder.file_count || 0} files
+
+
+ `;
+
+ // Click to navigate to folder
+ folderElement.addEventListener('click', () => {
+ this.selectFolder(folder.id);
+ });
+
+ return folderElement;
+ }
+
+ createFileElement(file) {
+ const fileElement = document.createElement('div');
+ fileElement.className = `file-item ${this.viewMode}-item`;
+ fileElement.setAttribute('data-file-id', file.id);
+
+ const thumbnail = this.getThumbnail(file);
+ const tags = file.tags.map(tag => `${this.escapeHtml(tag)} `).join('');
+
+ fileElement.innerHTML = `
+
+ ${thumbnail}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${this.escapeHtml(file.title || file.original_filename)}
+
+
+ ${file.file_size_formatted}
+ .${file.file_type.toUpperCase()}
+ ${this.formatDate(file.created)}
+
+
+ ${tags}
+
+
+ `;
+
+ // Bind events
+ fileElement.querySelector('.preview-btn').addEventListener('click', () => {
+ this.previewFile(file);
+ });
+
+ fileElement.querySelector('.download-btn').addEventListener('click', () => {
+ this.downloadFile(file);
+ });
+
+ fileElement.querySelector('.edit-btn').addEventListener('click', () => {
+ this.editFile(file);
+ });
+
+ // Make tags clickable to filter by tag
+ fileElement.querySelectorAll('.tag.clickable').forEach(tagElement => {
+ tagElement.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const tagName = tagElement.getAttribute('data-tag');
+ this.filterByTag(tagName);
+ });
+ });
+
+ fileElement.addEventListener('dblclick', () => {
+ this.previewFile(file);
+ });
+
+ return fileElement;
+ }
+
+ getThumbnail(file) {
+ const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.file_type.toLowerCase());
+
+ if (isImage && file.thumbnail_path) {
+ return ` `;
+ }
+
+ // File type icons
+ const iconMap = {
+ pdf: 'fa-file-pdf',
+ doc: 'fa-file-word',
+ docx: 'fa-file-word',
+ xls: 'fa-file-excel',
+ xlsx: 'fa-file-excel',
+ mp4: 'fa-file-video',
+ mov: 'fa-file-video',
+ avi: 'fa-file-video'
+ };
+
+ const iconClass = iconMap[file.file_type.toLowerCase()] || 'fa-file';
+
+ return `
`;
+ }
+
+ showUploadModal() {
+ const modal = document.getElementById('uploadModal');
+ this.showModal(modal);
+ this.populateUploadFolders(this.folders); // Use stored folders data
+ }
+
+ showFolderModal() {
+ const modal = document.getElementById('folderModal');
+ this.showModal(modal);
+ this.populateParentFolders(this.folders); // Use stored folders data
+ }
+
+ showModal(modal) {
+ modal.style.display = 'flex';
+ modal.classList.add('show');
+ document.body.classList.add('modal-open');
+ }
+
+ closeModal(modal) {
+ modal.classList.remove('show');
+ setTimeout(() => {
+ modal.style.display = 'none';
+ document.body.classList.remove('modal-open');
+ }, 300);
+ }
+
+ handleFileSelect(files) {
+ this.uploadQueue = [];
+
+ Array.from(files).forEach(file => {
+ this.uploadQueue.push({
+ file: file,
+ progress: 0,
+ status: 'pending'
+ });
+ });
+
+ this.renderUploadQueue();
+ document.getElementById('startUpload').disabled = this.uploadQueue.length === 0;
+ }
+
+ renderUploadQueue() {
+ const container = document.getElementById('uploadQueue');
+ container.innerHTML = '';
+
+ this.uploadQueue.forEach((item, index) => {
+ const queueItem = document.createElement('div');
+ queueItem.className = 'upload-item';
+ queueItem.innerHTML = `
+
+
${this.escapeHtml(item.file.name)}
+
${this.formatFileSize(item.file.size)}
+
+
+
+
+
+ `;
+
+ queueItem.querySelector('.remove-btn').addEventListener('click', () => {
+ this.removeFromQueue(index);
+ });
+
+ container.appendChild(queueItem);
+ });
+ }
+
+ async startUpload() {
+ const folder = document.getElementById('uploadFolder').value;
+ const tags = document.getElementById('uploadTags').value
+ .split(',')
+ .map(tag => tag.trim())
+ .filter(tag => tag.length > 0);
+
+ for (let i = 0; i < this.uploadQueue.length; i++) {
+ const item = this.uploadQueue[i];
+ await this.uploadFile(item, folder, tags, i);
+ }
+
+ // Switch to the uploaded folder if different from current
+ if (folder && folder !== this.currentFolder) {
+ this.currentFolder = folder;
+ }
+
+ this.loadFiles();
+ this.closeModal(document.getElementById('uploadModal'));
+ this.showToast('Files uploaded successfully!', 'success');
+ }
+
+ async uploadFile(item, folderId, tags, index) {
+ const formData = new FormData();
+ formData.append('file', item.file);
+ formData.append('folder_id', folderId);
+ formData.append('tags', JSON.stringify(tags));
+ formData.append('title', item.file.name.replace(/\.[^/.]+$/, ""));
+
+ item.status = 'uploading';
+ this.updateQueueItem(index, item);
+
+ try {
+ const response = await fetch('index.php?page=marketing&action=marketing_upload', {
+ method: 'POST',
+ body: formData,
+ onUploadProgress: (progressEvent) => {
+ item.progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+ this.updateQueueItem(index, item);
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+ if (!text || text.trim() === '') {
+ throw new Error('Empty response from upload server');
+ }
+
+ const result = JSON.parse(text);
+
+ if (result.success) {
+ item.status = 'completed';
+ item.progress = 100;
+ } else {
+ throw new Error(result.error || 'Upload failed');
+ }
+ } catch (error) {
+ item.status = 'error';
+ item.error = error.message;
+ this.showToast(error.message, 'error');
+ }
+
+ this.updateQueueItem(index, item);
+ }
+
+ async createFolder() {
+ const folderName = document.getElementById('folderName').value.trim();
+ const parentId = document.getElementById('parentFolder').value;
+ const description = document.getElementById('folderDescription').value.trim();
+
+ if (!folderName) {
+ this.showToast('Folder name is required', 'error');
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('folder_name', folderName);
+ formData.append('parent_id', parentId || '');
+ formData.append('description', description);
+
+ const response = await fetch('index.php?page=marketing&action=marketing_folders', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+ if (!text || text.trim() === '') {
+ throw new Error('Empty response from server');
+ }
+
+ const data = JSON.parse(text);
+
+ if (data && (data.success || data.rowID)) {
+ this.closeModal(document.getElementById('folderModal'));
+ this.loadFolders();
+ this.showToast('Folder created successfully!', 'success');
+ } else if (data.error) {
+ throw new Error(data.error);
+ } else {
+ throw new Error('Unexpected response format');
+ }
+ } catch (error) {
+ console.error('Create folder error:', error);
+ this.showToast(error.message || 'Error creating folder', 'error');
+ }
+ }
+
+ async deleteFile(file) {
+ if (!confirm(`Are you sure you want to delete "${file.title || file.original_filename}"?`)) {
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('file_id', file.id);
+
+ const response = await fetch('index.php?page=marketing&action=marketing_delete', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+ if (!text || text.trim() === '') {
+ throw new Error('Empty response from delete server');
+ }
+
+ const data = JSON.parse(text);
+
+ if (data && (data.success || !data.error)) {
+ this.closeModal(document.getElementById('previewModal'));
+ this.loadFiles();
+ this.showToast('File deleted successfully!', 'success');
+ } else if (data.error) {
+ throw new Error(data.error);
+ } else {
+ throw new Error('Unexpected response format');
+ }
+ } catch (error) {
+ this.showToast(error.message || 'Error deleting file', 'error');
+ }
+ }
+
+ previewFile(file) {
+ this.selectedFile = file;
+ const modal = document.getElementById('previewModal');
+ const title = document.getElementById('previewTitle');
+ const content = document.getElementById('previewContent');
+
+ title.textContent = file.title || file.original_filename;
+
+ // Generate preview content based on file type
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.file_type.toLowerCase())) {
+ content.innerHTML = ` `;
+ } else if (file.file_type.toLowerCase() === 'mp4') {
+ content.innerHTML = ` `;
+ } else {
+ content.innerHTML = `
+
+
+
${this.escapeHtml(file.title || file.original_filename)}
+
File Type: ${file.file_type.toUpperCase()}
+
Size: ${file.file_size_formatted}
+
Created: ${this.formatDate(file.created)}
+
+ `;
+ }
+
+ this.showModal(modal);
+ }
+
+ downloadFile(file) {
+ const link = document.createElement('a');
+ link.href = file.file_path;
+ link.download = file.original_filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+
+ // Utility methods
+ async apiCall(endpoint, params = {}, method = 'GET') {
+ const url = new URL(`/api.php${endpoint}`, window.location.origin);
+
+ let options = {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ };
+
+ if (method === 'GET') {
+ Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
+ } else {
+ options.body = JSON.stringify(params);
+ }
+
+ const response = await fetch(url, options);
+ return await response.json();
+ }
+
+ escapeHtml(text) {
+ const map = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ };
+ return text ? text.replace(/[&<>"']/g, m => map[m]) : '';
+ }
+
+ formatDate(dateString) {
+ return new Date(dateString).toLocaleDateString();
+ }
+
+ formatFileSize(bytes) {
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
+ }
+
+ debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ }
+
+ showToast(message, type = 'info') {
+ // Simple toast implementation
+ const toast = document.createElement('div');
+ toast.className = `toast toast-${type}`;
+ toast.textContent = message;
+
+ document.body.appendChild(toast);
+
+ setTimeout(() => {
+ toast.classList.add('show');
+ }, 100);
+
+ setTimeout(() => {
+ toast.classList.remove('show');
+ setTimeout(() => document.body.removeChild(toast), 300);
+ }, 3000);
+ }
+
+ setViewMode(mode) {
+ this.viewMode = mode;
+ const container = document.getElementById('filesContainer');
+
+ // Update view mode classes
+ container.className = `files-container ${mode}-view`;
+
+ // Update button states
+ document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
+ document.getElementById(`${mode}ViewBtn`).classList.add('active');
+
+ // Re-render files with new view mode
+ this.loadFiles();
+ }
+
+ selectFolder(folderId) {
+ // Clear current folder selection and files BEFORE setting new folder
+ const container = document.getElementById('filesContainer');
+ container.innerHTML = '';
+
+ // Set new current folder
+ this.currentFolder = folderId;
+
+ // Update UI
+ this.updateBreadcrumb();
+
+ // Update active folder in tree
+ document.querySelectorAll('.folder-item').forEach(item => {
+ item.classList.remove('active');
+ });
+ const selectedFolder = document.querySelector(`[data-folder="${folderId}"]`);
+ if (selectedFolder) {
+ selectedFolder.classList.add('active');
+ }
+
+ // Load files for the new folder
+ this.loadFiles();
+ }
+
+ updateBreadcrumb() {
+ // Implement breadcrumb navigation
+ const nav = document.getElementById('breadcrumbNav');
+ // This would build breadcrumb based on current folder path
+ }
+
+ updateFileTypeFilters() {
+ this.filters.fileTypes = [];
+
+ document.querySelectorAll('.file-type-filters input[type="checkbox"]:checked').forEach(checkbox => {
+ const types = checkbox.value.split(',');
+ this.filters.fileTypes.push(...types);
+ });
+
+ this.loadFiles();
+ }
+
+ filterByTag(tagName) {
+ // Set the tag filter
+ this.filters.tag = tagName;
+
+ // Update the dropdown to show the selected tag
+ const tagSelect = document.getElementById('tagFilter');
+ if (tagSelect) {
+ tagSelect.value = tagName;
+ }
+
+ // Clear folder selection to search across all folders
+ this.currentFolder = '';
+
+ // Update folder tree UI to show root as active
+ document.querySelectorAll('.folder-item').forEach(item => {
+ item.classList.remove('active');
+ });
+ const rootFolder = document.querySelector('.folder-item.root');
+ if (rootFolder) {
+ rootFolder.classList.add('active');
+ }
+
+ // Reload files with the tag filter
+ this.loadFiles();
+ }
+
+ populateTagFilter(tags) {
+ const select = document.getElementById('tagFilter');
+ select.innerHTML = 'All Tags ';
+
+ tags.forEach(tag => {
+ const option = document.createElement('option');
+ option.value = tag.tag_name;
+ option.textContent = `${tag.tag_name} (${tag.usage_count})`;
+ select.appendChild(option);
+ });
+ }
+
+ populateFolderSelects(folders) {
+ this.populateUploadFolders(folders);
+ this.populateParentFolders(folders);
+ }
+
+ addFolderOptions(select, folders, level = 0) {
+ folders.forEach(folder => {
+ const option = document.createElement('option');
+ option.value = folder.id;
+ option.textContent = ' '.repeat(level) + folder.folder_name;
+ select.appendChild(option);
+
+ if (folder.children && folder.children.length > 0) {
+ this.addFolderOptions(select, folder.children, level + 1);
+ }
+ });
+ }
+
+ populateUploadFolders(folders = []) {
+ // Populate upload folder select
+ const select = document.getElementById('uploadFolder');
+ if (select) {
+ select.innerHTML = 'Root Folder ';
+ this.addFolderOptions(select, folders);
+ }
+ }
+
+ populateParentFolders(folders = []) {
+ // Populate parent folder select
+ const select = document.getElementById('parentFolder');
+ if (select) {
+ select.innerHTML = 'Root Folder ';
+ this.addFolderOptions(select, folders);
+ }
+ }
+
+ getFileIcon(fileType) {
+ const iconMap = {
+ pdf: 'fa-file-pdf',
+ doc: 'fa-file-word',
+ docx: 'fa-file-word',
+ xls: 'fa-file-excel',
+ xlsx: 'fa-file-excel',
+ mp4: 'fa-file-video',
+ mov: 'fa-file-video',
+ avi: 'fa-file-video'
+ };
+
+ return iconMap[fileType.toLowerCase()] || 'fa-file';
+ }
+
+ // Edit file functionality
+ editFile(file) {
+ this.selectedFile = file;
+ this.showEditModal();
+ this.populateEditModal(file);
+ }
+
+ showEditModal() {
+ const modal = document.getElementById('editModal');
+ if (modal) {
+ this.showModal(modal);
+ }
+ }
+
+ populateEditModal(file) {
+ // Populate title
+ const titleInput = document.getElementById('editTitle');
+ if (titleInput) {
+ titleInput.value = file.title || '';
+ }
+
+ // Populate folder select
+ const folderSelect = document.getElementById('editFolder');
+ if (folderSelect) {
+ folderSelect.innerHTML = 'Root Folder ';
+ this.addFolderOptions(folderSelect, this.folders);
+
+ // Select current folder
+ if (file.folder_id) {
+ folderSelect.value = file.folder_id;
+ }
+ }
+
+ // Populate tags
+ const tagsInput = document.getElementById('editTags');
+ if (tagsInput) {
+ tagsInput.value = file.tags ? file.tags.join(', ') : '';
+ }
+ }
+
+ saveEdit() {
+ if (!this.selectedFile) return;
+
+ const title = document.getElementById('editTitle').value.trim();
+ const folderId = document.getElementById('editFolder').value;
+ const tags = document.getElementById('editTags').value.trim();
+
+ // Compare with original values to detect changes
+ const originalTitle = this.selectedFile.title || '';
+ const originalFolderId = this.selectedFile.folder_id || '';
+ const originalTags = (this.selectedFile.tags || []).join(', ');
+
+ const hasChanges = title !== originalTitle ||
+ folderId !== originalFolderId ||
+ tags !== originalTags;
+
+ if (!hasChanges) {
+ this.showToast('No changes detected', 'info');
+ this.closeModal(document.getElementById('editModal'));
+ return;
+ }
+
+ // Show loading state
+ const saveBtn = document.getElementById('saveEdit');
+ const originalText = saveBtn.innerHTML;
+ saveBtn.innerHTML = ' Saving...';
+ saveBtn.disabled = true;
+
+ // Prepare FormData with only changed fields
+ const formData = new FormData();
+ formData.append('file_id', this.selectedFile.id);
+
+ if (title !== originalTitle) {
+ formData.append('title', title);
+ }
+ if (folderId !== originalFolderId) {
+ formData.append('folder_id', folderId);
+ }
+ if (tags !== originalTags) {
+ formData.append('tags', tags);
+ }
+
+ // Send update request
+ fetch('index.php?page=marketing&action=marketing_update', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ this.showToast('File updated successfully!', 'success');
+ // Reload window to reflect changes
+ setTimeout(() => {
+ window.location.reload();
+ }, 500);
+ } else {
+ throw new Error(data.error || data.message || 'Failed to update file');
+ }
+ })
+ .catch(error => {
+ console.error('Update error:', error);
+ this.showToast('Error updating file: ' + error.message, 'error');
+ })
+ .finally(() => {
+ // Restore button state
+ saveBtn.innerHTML = originalText;
+ saveBtn.disabled = false;
+ });
+ }
+
+ updateQueueItem(index, item) {
+ const queueItems = document.querySelectorAll('.upload-item');
+ if (queueItems[index]) {
+ const progressFill = queueItems[index].querySelector('.progress-fill');
+ const status = queueItems[index].querySelector('.upload-status');
+
+ progressFill.style.width = `${item.progress}%`;
+ status.textContent = item.status;
+
+ if (item.status === 'error') {
+ queueItems[index].classList.add('error');
+ } else if (item.status === 'completed') {
+ queueItems[index].classList.add('completed');
+ }
+ }
+ }
+
+ removeFromQueue(index) {
+ this.uploadQueue.splice(index, 1);
+ this.renderUploadQueue();
+ document.getElementById('startUpload').disabled = this.uploadQueue.length === 0;
+ }
+
+ // Edit folder functionality
+ editFolder(folder) {
+ this.selectedFolder = folder;
+ this.showEditFolderModal();
+ this.populateEditFolderModal(folder);
+ }
+
+ showEditFolderModal() {
+ const modal = document.getElementById('editFolderModal');
+ if (modal) {
+ this.showModal(modal);
+ }
+ }
+
+ populateEditFolderModal(folder) {
+ // Populate folder name
+ const nameInput = document.getElementById('editFolderName');
+ if (nameInput) {
+ nameInput.value = folder.folder_name || '';
+ }
+
+ // Populate parent folder select
+ const parentSelect = document.getElementById('editParentFolder');
+ if (parentSelect) {
+ parentSelect.innerHTML = 'Root Folder ';
+ this.addFolderOptionsExcluding(parentSelect, this.folders, folder.id);
+
+ // Select current parent
+ if (folder.parent_id) {
+ parentSelect.value = folder.parent_id;
+ }
+ }
+
+ // Populate description
+ const descInput = document.getElementById('editFolderDescription');
+ if (descInput) {
+ descInput.value = folder.description || '';
+ }
+ }
+
+ addFolderOptionsExcluding(select, folders, excludeId, level = 0) {
+ // Add folders but exclude the current folder and its children
+ folders.forEach(folder => {
+ if (folder.id !== excludeId && !this.isFolderDescendant(folder, excludeId)) {
+ const option = document.createElement('option');
+ option.value = folder.id;
+ option.textContent = ' '.repeat(level) + folder.folder_name;
+ select.appendChild(option);
+
+ if (folder.children && folder.children.length > 0) {
+ this.addFolderOptionsExcluding(select, folder.children, excludeId, level + 1);
+ }
+ }
+ });
+ }
+
+ isFolderDescendant(folder, ancestorId) {
+ // Check if folder is a descendant of ancestorId
+ if (folder.id === ancestorId) return true;
+ if (folder.children) {
+ for (let child of folder.children) {
+ if (this.isFolderDescendant(child, ancestorId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ saveEditFolder() {
+ if (!this.selectedFolder) return;
+
+ const folderName = document.getElementById('editFolderName').value.trim();
+ const parentId = document.getElementById('editParentFolder').value;
+ const description = document.getElementById('editFolderDescription').value.trim();
+
+ // Compare with original values
+ const originalName = this.selectedFolder.folder_name || '';
+ const originalParentId = this.selectedFolder.parent_id || '';
+ const originalDescription = this.selectedFolder.description || '';
+
+ const hasChanges = folderName !== originalName ||
+ parentId !== originalParentId ||
+ description !== originalDescription;
+
+ if (!hasChanges) {
+ this.showToast('No changes detected', 'info');
+ this.closeModal(document.getElementById('editFolderModal'));
+ return;
+ }
+
+ if (!folderName) {
+ this.showToast('Folder name is required', 'error');
+ return;
+ }
+
+ // Show loading state
+ const saveBtn = document.getElementById('saveEditFolder');
+ const originalText = saveBtn.innerHTML;
+ saveBtn.innerHTML = ' Saving...';
+ saveBtn.disabled = true;
+
+ // Prepare FormData
+ const formData = new FormData();
+ formData.append('id', this.selectedFolder.id);
+
+ if (folderName !== originalName) {
+ formData.append('folder_name', folderName);
+ }
+ if (parentId !== originalParentId) {
+ formData.append('parent_id', parentId);
+ }
+ if (description !== originalDescription) {
+ formData.append('description', description);
+ }
+
+ // Send update request
+ fetch('index.php?page=marketing&action=marketing_folders', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ this.showToast('Folder updated successfully!', 'success');
+ // Reload window to reflect changes
+ setTimeout(() => {
+ window.location.reload();
+ }, 500);
+ } else {
+ throw new Error(data.error || data.message || 'Failed to update folder');
+ }
+ })
+ .catch(error => {
+ console.error('Update folder error:', error);
+ this.showToast('Error updating folder: ' + error.message, 'error');
+ })
+ .finally(() => {
+ // Restore button state
+ saveBtn.innerHTML = originalText;
+ saveBtn.disabled = false;
+ });
+ }
+
+ async deleteFolder(folder) {
+ if (!confirm(`Are you sure you want to delete the folder "${folder.folder_name}"? This action cannot be undone.`)) {
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('id', folder.id);
+ formData.append('delete', 'true');
+
+ const response = await fetch('index.php?page=marketing&action=marketing_folders', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const text = await response.text();
+ if (!text || text.trim() === '') {
+ throw new Error('Empty response from server');
+ }
+
+ const data = JSON.parse(text);
+
+ if (data && (data.success || !data.error)) {
+ this.closeModal(document.getElementById('editFolderModal'));
+ this.showToast('Folder deleted successfully!', 'success');
+ // Reload window to reflect changes
+ setTimeout(() => {
+ window.location.reload();
+ }, 500);
+ } else if (data.error) {
+ throw new Error(data.error);
+ } else {
+ throw new Error('Unexpected response format');
+ }
+ } catch (error) {
+ this.showToast(error.message || 'Error deleting folder', 'error');
+ }
+ }
+}
+
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', () => {
+ window.marketingManager = new MarketingFileManager();
+});
\ No newline at end of file
diff --git a/assets/mollie/.DS_Store b/assets/mollie/.DS_Store
deleted file mode 100644
index 9c0e967..0000000
Binary files a/assets/mollie/.DS_Store and /dev/null differ
diff --git a/assets/mollie/src/.DS_Store b/assets/mollie/src/.DS_Store
deleted file mode 100644
index c6ec583..0000000
Binary files a/assets/mollie/src/.DS_Store and /dev/null differ
diff --git a/assets/scripts.js b/assets/scripts.js
index be9dad7..6a2744d 100644
--- a/assets/scripts.js
+++ b/assets/scripts.js
@@ -124,7 +124,7 @@ async function connectDevice() {
// Log connection failure details
await logCommunication(`Serial connection failed: ${error.message || 'Unknown error'}`, 'disconnected');
- if (openPort === 1){
+ if (openPort = 1){
closePort();
console.log("Closing port");
alert("System is still trying to close the serial port. If this message continues to come up please refresh this page.");
diff --git a/assets/softwaretool.js b/assets/softwaretool.js
index 0194de5..a8855c8 100644
--- a/assets/softwaretool.js
+++ b/assets/softwaretool.js
@@ -10,11 +10,135 @@ let deviceVersion = "";
let deviceHwVersion = "";
let selectedSoftwareUrl = "";
+// Helper function to generate country select options
+function generateCountryOptions(selectedCountry = '') {
+ if (typeof COUNTRIES === 'undefined' || !COUNTRIES) {
+ return `${typeof TRANS_COUNTRY !== 'undefined' ? TRANS_COUNTRY : 'Country'} `;
+ }
+
+ // Sort countries alphabetically
+ const sortedCountries = Object.values(COUNTRIES).sort((a, b) => {
+ return a.country.localeCompare(b.country);
+ });
+
+ let options = 'Select country ';
+ sortedCountries.forEach(data => {
+ const selected = (selectedCountry === data.country) ? 'selected' : '';
+ options += `${data.country} `;
+ });
+
+ return options;
+}
+
// Serial port variables (port, writer, textEncoder, writableStreamClosed declared in PHP)
let reader;
let readableStreamClosed;
let keepReading = true;
+// Browser compatibility check
+let isSerialSupported = false;
+
+// Check browser compatibility on page load
+function checkBrowserCompatibility() {
+ isSerialSupported = 'serial' in navigator;
+
+ if (!isSerialSupported) {
+ // Show warning banner
+ showBrowserWarningBanner();
+ // Disable connect button
+ disableSerialFunctionality();
+ }
+
+ return isSerialSupported;
+}
+
+function showBrowserWarningBanner() {
+ const connectDevice = document.getElementById("connectdevice");
+ if (!connectDevice) return;
+
+ const warningBanner = document.createElement("div");
+ warningBanner.id = "browserWarningBanner";
+ warningBanner.style.cssText = `
+ background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
+ color: white;
+ padding: 15px 20px;
+ border-radius: 8px;
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
+ `;
+
+ warningBanner.innerHTML = `
+
+
+
Browser Not Supported
+
+ Please use Chrome , Edge , or Opera to access device connectivity features.
+
+
+ `;
+
+ connectDevice.parentNode.insertBefore(warningBanner, connectDevice);
+}
+
+function disableSerialFunctionality() {
+ const connectButton = document.getElementById("connectButton");
+ if (connectButton) {
+ connectButton.disabled = true;
+ connectButton.style.opacity = "0.5";
+ connectButton.style.cursor = "not-allowed";
+ connectButton.title = "Browser is not supported. Please use Chrome, Edge, or Opera.";
+ }
+}
+
+// Call on page load
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', checkBrowserCompatibility);
+} else {
+ checkBrowserCompatibility();
+}
+
+// Shared serial port reference for upload.js to use
+window.sharedSerialPort = null;
+
+// Override requestPort to minimize user prompts
+// This intercepts all requestPort calls (including from upload.js) to reuse authorized ports
+if ('serial' in navigator) {
+ const originalRequestPort = navigator.serial.requestPort.bind(navigator.serial);
+
+ navigator.serial.requestPort = async function(options) {
+ // If we have a shared port, return it instead of prompting
+ if (window.sharedSerialPort) {
+ console.log('Using shared serial port (no prompt needed)');
+ return window.sharedSerialPort;
+ }
+
+ // Try already-authorized ports matching the filters
+ const ports = await navigator.serial.getPorts();
+ if (ports.length > 0 && options?.filters) {
+ const match = ports.find(p => {
+ const info = p.getInfo();
+ return options.filters.some(f =>
+ info.usbVendorId === f.usbVendorId &&
+ info.usbProductId === f.usbProductId
+ );
+ });
+ if (match) {
+ console.log('Using previously authorized port (no prompt needed)');
+ window.sharedSerialPort = match;
+ return match;
+ }
+ }
+
+ // Fallback: original prompt behavior
+ const port = await originalRequestPort(options);
+ window.sharedSerialPort = port; // Store for future use
+ return port;
+ };
+}
+
// Function to log communication to API (reused from scripts.js)
async function logCommunication(data, direction) {
// Only log if debug mode is enabled
@@ -69,14 +193,37 @@ function progressBar(percentage, message, color){
// Connect device for software tool
async function connectDeviceForSoftware() {
+ // Browser compatibility check
+ if (!isSerialSupported) {
+ progressBar("100", "Browser not supported - Please use Chrome, Edge, or Opera", "#dc3545");
+ await logCommunication('Connection attempt failed: Web Serial API not supported in browser', 'error');
+ return;
+ }
+
//clear input
readBar.innerHTML = '';
serialResultsDiv.innerHTML = '';
+
+ // Clear installation status if it exists
+ const installStatus = document.getElementById("installationStatus");
+ if (installStatus) {
+ installStatus.remove();
+ }
+
document.getElementById("softwareCheckStatus").style.display = "none";
- document.getElementById("softwareOptions").style.display = "none";
+ document.getElementById("softwareOptionsContainer").style.display = "none";
document.getElementById("noUpdatesMessage").style.display = "none";
document.getElementById("uploadSection").style.display = "none";
+ // Reset softwareOptions visibility and blur state
+ const softwareOptions = document.getElementById("softwareOptions");
+ if (softwareOptions) {
+ softwareOptions.style.display = "block";
+ softwareOptions.style.filter = "blur(8px)";
+ softwareOptions.style.opacity = "0.3";
+ softwareOptions.style.pointerEvents = "none";
+ }
+
// Reset data
receivedDataBuffer = '';
deviceSerialNumber = "";
@@ -87,7 +234,7 @@ async function connectDeviceForSoftware() {
progressBar("1", "", "");
// Check if DEBUG mode is enabled - use mock device data
- if (typeof DEBUG !== 'undefined' && DEBUG) {
+ if (typeof DEBUG !== 'undefined' && DEBUG && typeof DEBUG_ID !== 'undefined' && DEBUG_ID) {
// TEST MODE: Use mock device data
deviceSerialNumber = "22110095";
deviceVersion = "03e615af";
@@ -161,7 +308,20 @@ async function connectDeviceForSoftware() {
} catch (error) {
await logCommunication(`Connection error: ${error.message}`, 'error');
- progressBar("0", "Error: " + error.message, "#ff6666");
+
+ // Improved error messages with specific cases
+ if (error.name === 'NotSupportedError' || !navigator.serial) {
+ progressBar("100", "Browser not supported - Please use Chrome, Edge, or Opera", "#dc3545");
+ } else if (error.message && error.message.includes('No port selected by the user')) {
+ progressBar("100", "No device selected - Please try again", "#ff6666");
+ } else if (error.name === 'NetworkError') {
+ progressBar("100", "Connection failed - Please check device connection", "#ff6666");
+ } else if (error.name === 'InvalidStateError') {
+ progressBar("100", "Port already in use - Refreshing page...", "#ff9800");
+ setTimeout(() => location.reload(), 2000);
+ } else {
+ progressBar("100", "Connection error: " + error.message, "#ff6666");
+ }
}
}
@@ -295,7 +455,11 @@ async function closePortAfterRead() {
await port.close();
await logCommunication('Port closed successfully', 'info');
- // Reset for next connection
+ // Keep port reference in sharedSerialPort for upload.js to reuse
+ // This prevents the need for another user prompt during firmware upload
+ window.sharedSerialPort = port;
+
+ // Reset local variables for next connection
reader = null;
writer = null;
readableStreamClosed = null;
@@ -305,7 +469,12 @@ async function closePortAfterRead() {
console.error('Error closing port after read:', error);
await logCommunication(`Error closing port: ${error.message}`, 'error');
- // Force reset even on error
+ // Keep port reference even on error if port exists
+ if (port) {
+ window.sharedSerialPort = port;
+ }
+
+ // Force reset local variables even on error
reader = null;
writer = null;
readableStreamClosed = null;
@@ -437,12 +606,28 @@ async function fetchSoftwareOptions() {
return;
}
- // Display options in table
+ // Display options in table (blurred initially)
displaySoftwareOptions(options);
document.getElementById("softwareCheckStatus").style.display = "none";
- document.getElementById("softwareOptions").style.display = "block";
+ document.getElementById("softwareOptionsContainer").style.display = "block";
progressBar("100", "Software options loaded", "#04AA6D");
+ // Check if customer data already exists in sessionStorage
+ const savedCustomerData = sessionStorage.getItem('customerData');
+
+ // Show user info modal only if no saved data and not in debug mode
+ if ((typeof DEBUG === 'undefined' || !DEBUG || typeof DEBUG_ID === 'undefined' || !DEBUG_ID) && !savedCustomerData) {
+ showUserInfoModal();
+ } else {
+ // Customer data already exists or debug mode - reveal software options immediately
+ const softwareOptions = document.getElementById("softwareOptions");
+ if (softwareOptions) {
+ softwareOptions.style.filter = "none";
+ softwareOptions.style.opacity = "1";
+ softwareOptions.style.pointerEvents = "auto";
+ }
+ }
+
} catch (error) {
await logCommunication(`Software options error: ${error.message}`, 'error');
progressBar("0", "Error loading options: " + error.message, "#ff6666");
@@ -458,6 +643,8 @@ function displaySoftwareOptions(options) {
const price = parseFloat(option.price);
const isFree = price === 0;
const isCurrent = option.is_current === true || option.is_current === 1;
+ const dealerInfo = option.dealer_info || {};
+ const isDealer = dealerInfo.is_dealer === 1 || dealerInfo.is_dealer === '1';
// Create card with gradient background
const card = document.createElement("div");
@@ -562,92 +749,328 @@ function displaySoftwareOptions(options) {
margin-top: auto;
`;
- const priceText = document.createElement("div");
- priceText.style.cssText = `
- 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;
- `;
+ // Check if this is a dealer customer - show dealer contact info instead of price/buy button
+ if (isDealer && !isCurrent && !isFree) {
+ // Dealer info section - replaces price and buy button
+ const dealerSection = document.createElement("div");
+ dealerSection.style.cssText = `
+ background: linear-gradient(135deg, rgb(255, 107, 53) 0%, rgb(255, 69, 0) 100%);
+ border-radius: 4px;
+ padding: 15px;
+ text-align: center;
+ `;
- if (isCurrent) {
- priceText.innerHTML = ' INSTALLED';
+ // Contact dealer message
+ const dealerMessage = document.createElement("div");
+ dealerMessage.style.cssText = `
+ color: white;
+ font-size: 14px;
+ font-weight: 600;
+ margin-bottom: 12px;
+ `;
+ dealerMessage.innerHTML = ' Contact your dealer for pricing and upgrade options';
+ dealerSection.appendChild(dealerMessage);
+
+ // Dealer contact details
+ const dealerDetails = document.createElement("div");
+ dealerDetails.style.cssText = `
+ background: white;
+ border-radius: 4px;
+ padding: 12px;
+ text-align: left;
+ font-size: 13px;
+ color: #333;
+ `;
+
+ let dealerHtml = '';
+ if (dealerInfo.name) {
+ dealerHtml += ` ${dealerInfo.name}
`;
+ }
+ if (dealerInfo.address || dealerInfo.city || dealerInfo.postalcode || dealerInfo.country) {
+ dealerHtml += ``;
+ if (dealerInfo.address) {
+ dealerHtml += `
${dealerInfo.address}
`;
+ }
+ if (dealerInfo.postalcode || dealerInfo.city) {
+ dealerHtml += `
${[dealerInfo.postalcode, dealerInfo.city].filter(Boolean).join(' ')}
`;
+ }
+ if (dealerInfo.country) {
+ dealerHtml += `
${dealerInfo.country}
`;
+ }
+ dealerHtml += `
`;
+ }
+ if (dealerInfo.email) {
+ dealerHtml += ``;
+ }
+ if (dealerInfo.phone) {
+ dealerHtml += ``;
+ }
+
+ dealerDetails.innerHTML = dealerHtml;
+ dealerSection.appendChild(dealerDetails);
+ priceSection.appendChild(dealerSection);
} else {
- priceText.innerHTML = isFree
- ? 'Free'
- : `${option.currency || "€"} ${price.toFixed(2)}`;
+ // Standard price display for non-dealer customers
+ const priceText = document.createElement("div");
+ priceText.style.cssText = `
+ 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.innerHTML = ' INSTALLED';
+ } else {
+ priceText.innerHTML = isFree
+ ? 'Free'
+ : `${option.currency || "€"} ${price.toFixed(2)} (excl. VAT) `;
+ }
+
+ priceSection.appendChild(priceText);
+
+ // Action button with gradient for paid
+ const actionBtn = document.createElement("button");
+ actionBtn.className = "btn";
+ actionBtn.style.cssText = `
+ width: 100%;
+ background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')};
+ color: white;
+ border: none;
+ cursor: ${isCurrent ? 'not-allowed' : 'pointer'};
+ 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) {
+ actionBtn.innerHTML = ' Currently Installed';
+ actionBtn.disabled = true;
+ } else if (isFree) {
+ actionBtn.innerHTML = ' ';
+ actionBtn.onclick = () => selectUpgrade(option);
+ 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 = ' ';
+ actionBtn.onclick = () => selectUpgrade(option);
+ 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);
}
- priceSection.appendChild(priceText);
-
- // Action button with gradient for paid
- const actionBtn = document.createElement("button");
- actionBtn.className = "btn";
- actionBtn.style.cssText = `
- width: 100%;
- background: ${isCurrent ? '#6c757d' : (isFree ? 'linear-gradient(135deg, #04AA6D 0%, #038f5a 100%)' : 'linear-gradient(135deg, #FF6B35 0%, #FF4500 100%)')};
- color: white;
- border: none;
- cursor: ${isCurrent ? 'not-allowed' : 'pointer'};
- 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) {
- actionBtn.innerHTML = ' Currently Installed';
- actionBtn.disabled = true;
- } else if (isFree) {
- actionBtn.innerHTML = ' ';
- actionBtn.onclick = () => selectUpgrade(option);
- 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 = ' ';
- actionBtn.onclick = () => selectUpgrade(option);
- 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);
-
card.appendChild(priceSection);
grid.appendChild(card);
});
}
+function showUserInfoModal() {
+ // Create modal overlay
+ const modal = document.createElement("div");
+ modal.id = "userInfoModal";
+ modal.style.cssText = `
+ display: flex;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.7);
+ z-index: 2000;
+ align-items: center;
+ justify-content: center;
+ `;
+
+ // Create modal content
+ const modalContent = document.createElement("div");
+ modalContent.style.cssText = `
+ background: white;
+ border-radius: 12px;
+ max-width: 500px;
+ width: 90%;
+ max-height: 90vh;
+ overflow-y: auto;
+ margin: 20px;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.4);
+ `;
+
+ modalContent.innerHTML = `
+
+
${typeof TRANS_USER_INFO_REQUIRED !== 'undefined' ? TRANS_USER_INFO_REQUIRED : 'User Information Required'}
+
${typeof TRANS_USER_INFO_DESCRIPTION !== 'undefined' ? TRANS_USER_INFO_DESCRIPTION : 'Please provide your information to continue with software updates'}
+
+
+ `;
+
+ modal.appendChild(modalContent);
+ document.body.appendChild(modal);
+
+ // Prefill form with customer data from sessionStorage if available
+ const savedCustomerData = sessionStorage.getItem('customerData');
+ if (savedCustomerData) {
+ try {
+ const customerData = JSON.parse(savedCustomerData);
+ if (customerData.name) document.getElementById("userInfoName").value = customerData.name;
+ if (customerData.email) document.getElementById("userInfoEmail").value = customerData.email;
+ if (customerData.address) document.getElementById("userInfoAddress").value = customerData.address;
+ if (customerData.city) document.getElementById("userInfoCity").value = customerData.city;
+ if (customerData.postal) document.getElementById("userInfoPostal").value = customerData.postal;
+ if (customerData.country) document.getElementById("userInfoCountry").value = customerData.country;
+ } catch (e) {
+ console.warn('Error parsing saved customer data:', e);
+ }
+ }
+
+ // Handle form submission
+ document.getElementById("userInfoForm").onsubmit = async (e) => {
+ e.preventDefault();
+ const formData = new FormData(e.target);
+ const customerData = {
+ name: formData.get("name"),
+ email: formData.get("email"),
+ address: formData.get("address"),
+ city: formData.get("city"),
+ postal: formData.get("postal"),
+ country: formData.get("country")
+ };
+
+ // Save customer data to sessionStorage
+ sessionStorage.setItem('customerData', JSON.stringify(customerData));
+
+ // Send to API
+ await sendUserInfoToAPI(customerData);
+
+ // Close modal
+ document.body.removeChild(modal);
+
+ // Reveal software options by removing blur
+ const softwareOptions = document.getElementById("softwareOptions");
+ if (softwareOptions) {
+ softwareOptions.style.filter = "none";
+ softwareOptions.style.opacity = "1";
+ softwareOptions.style.pointerEvents = "auto";
+ }
+ };
+}
+
+async function sendUserInfoToAPI(customerData) {
+ try {
+ const serviceToken = document.getElementById("servicetoken")?.innerHTML || '';
+ const url = link + '/v2/history';
+ const bearer = 'Bearer ' + serviceToken;
+
+ const historyData = {
+ sn: deviceSerialNumber,
+ type: 'customer',
+ sn_service: 'Portal',
+ payload: customerData
+ };
+
+ await logCommunication(`Sending user info to API: ${JSON.stringify(historyData)}`, 'sent');
+
+ const response = await fetch(url, {
+ method: 'POST',
+ withCredentials: true,
+ credentials: 'include',
+ headers: {
+ 'Authorization': bearer,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(historyData)
+ });
+
+ if (!response.ok) {
+ console.warn('Failed to send user info:', response.status);
+ await logCommunication(`Failed to send user info: ${response.status}`, 'error');
+ } else {
+ const result = await response.json();
+ console.log("User info sent successfully:", result);
+ await logCommunication(`User info sent successfully: ${JSON.stringify(result)}`, 'received');
+ }
+ } catch (error) {
+ console.warn('Error sending user info:', error);
+ await logCommunication(`Error sending user info: ${error.message}`, 'error');
+ }
+}
+
async function selectUpgrade(option) {
const price = parseFloat(option.price || 0);
const isFree = price === 0;
- // If paid upgrade, show payment modal first
+ // If paid upgrade, show payment modal with pre-filled data
if (!isFree) {
showPaymentModal(option);
return;
}
- // Free upgrade - show confirmation modal first
- showFreeInstallModal(option);
+ // Free upgrade - proceed directly with saved customer data
+ const savedCustomerData = sessionStorage.getItem('customerData');
+ if (savedCustomerData) {
+ try {
+ const customerData = JSON.parse(savedCustomerData);
+ await downloadAndInstallSoftware(option, customerData);
+ } catch (e) {
+ console.warn('Error parsing saved customer data:', e);
+ showFreeInstallModal(option);
+ }
+ } else {
+ showFreeInstallModal(option);
+ }
}
function showFreeInstallModal(option) {
@@ -693,22 +1116,24 @@ function showFreeInstallModal(option) {
';
diff --git a/communication.php b/communication.php
index 9fb97e3..452ac13 100644
--- a/communication.php
+++ b/communication.php
@@ -3,14 +3,14 @@ defined(page_security_key) or exit;
$page = 'communication';
//Check if allowed
-if (isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'R') === 0){
+if (isAllowed($page,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'R') === 0){
header('location: index.php');
exit;
}
//PAGE Security
-$update_allowed = isAllowed($page ,$_SESSION['profile'],$_SESSION['permission'],'U');
-$delete_allowed = isAllowed($page ,$_SESSION['profile'],$_SESSION['permission'],'D');
-$create_allowed = isAllowed($page ,$_SESSION['profile'],$_SESSION['permission'],'C');
+$update_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'U');
+$delete_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'D');
+$create_allowed = isAllowed($page ,$_SESSION['authorization']['permissions'],$_SESSION['authorization']['permission'],'C');
// Default input communication values
$communication = [
@@ -106,7 +106,7 @@ if ($delete_allowed === 1){
$view .= '