Add user roles management page and update user permissions handling
- Created a new `user_roles.php` file for managing user roles and permissions. - Implemented pagination, filtering, and sorting for user roles. - Updated `users.php` to use the new authorization structure for permissions. - Changed API version from v1 to v2 in `users.php` for user data retrieval. - Modified `webhook_mollie.php` to include account hierarchy in license creation. - Refactored invoice generation and email sending logic in `webhook_mollie.php`. - Introduced a new `webhook_paypal.php` file to handle PayPal webhook notifications. - Implemented payment status updates and license creation logic in `webhook_paypal.php`. - Added helper functions for PayPal webhook signature verification and access token retrieval.
This commit is contained in:
@@ -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('<html>');
|
||||
a.document.write('<body > ');
|
||||
a.document.write(divContents);
|
||||
a.document.write('</body></html>');
|
||||
a.document.close();
|
||||
a.print();
|
||||
var divContents = document.getElementById(div).innerHTML;
|
||||
var printWindow = window.open('', '', 'height=600,width=800');
|
||||
printWindow.document.write('<html><head><title>Print</title>');
|
||||
printWindow.document.write('<style>');
|
||||
printWindow.document.write('body { font-family: Arial, sans-serif; margin: 20px; }');
|
||||
printWindow.document.write('table { border-collapse: collapse; width: 100%; }');
|
||||
printWindow.document.write('th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }');
|
||||
printWindow.document.write('th { background-color: #f2f2f2; }');
|
||||
printWindow.document.write('</style>');
|
||||
printWindow.document.write('</head><body>');
|
||||
printWindow.document.write(divContents);
|
||||
printWindow.document.write('</body></html>');
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
printWindow.print();
|
||||
printWindow.close();
|
||||
}
|
||||
|
||||
//------------------------------------------
|
||||
|
||||
114
assets/database/marketing_install.sql
Normal file
114
assets/database/marketing_install.sql
Normal file
@@ -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
|
||||
222
assets/database/migration_profiles_to_rbac.sql
Normal file
222
assets/database/migration_profiles_to_rbac.sql
Normal file
@@ -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;
|
||||
141
assets/database/migration_users_to_rbac.sql
Normal file
141
assets/database/migration_users_to_rbac.sql
Normal file
@@ -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;
|
||||
65
assets/database/user_rbac
Normal file
65
assets/database/user_rbac
Normal file
@@ -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;
|
||||
1097
assets/functions.php
1097
assets/functions.php
File diff suppressed because it is too large
Load Diff
BIN
assets/images/TSS_invoice_footer.png
Normal file
BIN
assets/images/TSS_invoice_footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/images/TSS_invoice_header.png
Normal file
BIN
assets/images/TSS_invoice_header.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
164
assets/mail/email_template_invoice.php
Normal file
164
assets/mail/email_template_invoice.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
$payment_method = 'payment_method_'.$invoice_data['header']['payment_method'];
|
||||
|
||||
$message = '
|
||||
<!DOCTYPE html>
|
||||
<html lang="' . strtolower($language) . '">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>' . htmlspecialchars($lbl_invoice) . ' - Total Safety Solutions</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, Helvetica, sans-serif; color: #000000; font-size: 14px; line-height: 1.6; margin: 0; padding: 0; background-color: #f4f4f4;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: #f4f4f4;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="600" style="background-color: #ffffff; max-width: 600px;">
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px;">
|
||||
|
||||
<!-- Invoice Title -->
|
||||
<h1 style="font-size: 24px; font-weight: bold; color: #2c5f5d; margin: 0 0 25px 0;">' . htmlspecialchars($lbl_invoice) . '</h1>
|
||||
|
||||
<!-- Company Header -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 25px;">
|
||||
<tr>
|
||||
<td width="50%" style="vertical-align: top; font-size: 13px; line-height: 1.5;">
|
||||
<strong style="color: #2c5f5d; font-size: 14px;">Total Safety Solutions B.V.</strong><br>
|
||||
Laarakkerweg 8<br>
|
||||
5061 JR OISTERWIJK<br>
|
||||
Nederland
|
||||
</td>
|
||||
<td width="50%" style="vertical-align: top; text-align: left;">
|
||||
<strong style="color: #2c5f5d; font-size: 14px;">contact-details</strong><br>
|
||||
Ralf Adams<br>
|
||||
+31 13 8221480<br>
|
||||
ralfadams@totalsafetysolutions.nl
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Customer -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 25px;">
|
||||
<tr>
|
||||
<td width="50%" style="vertical-align: top; font-size: 13px; line-height: 1.5;">
|
||||
<strong style="color: #2c5f5d; font-size: 14px;">Customer</strong><br>
|
||||
'.$invoice_data['customer']['name'].'<br>
|
||||
'.$invoice_data['customer']['street'].'<br>
|
||||
'.$invoice_data['customer']['zip'].', '.$invoice_data['customer']['city'].'<br>
|
||||
'.$invoice_data['customer']['country'].'
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Invoice Details -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin-bottom: 25px; font-size: 13px;">
|
||||
<tr>
|
||||
<td width="50%" style="vertical-align: top; padding-right: 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 3px 0;"><strong>Invoice Date:</strong></td>
|
||||
<td style="padding: 3px 0;">' . htmlspecialchars(date('d-m-Y', strtotime($invoice_date))) . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 0;"><strong>Invoice Number:</strong></td>
|
||||
<td style="padding: 3px 0;">' . htmlspecialchars($order_id) . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 0;"><strong>Your Vat Number:</strong></td>
|
||||
<td style="padding: 3px 0;">' . htmlspecialchars($invoice_data['customer']['vat_number'] ?? '') . '</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td width="50%" style="vertical-align: top; padding-left: 10px;">
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 3px 0;"><strong>Reference:</strong></td>
|
||||
<td style="padding: 3px 0;">Online order</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 0;"><strong>Order number:</strong></td>
|
||||
<td style="padding: 3px 0;">' . htmlspecialchars($order_id) . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 0;"><strong>Payment Methodr:</strong></td>
|
||||
<td style="padding: 3px 0;">' . (${$payment_method} ?? $invoice_data['header']['payment_method'] ). '</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Items Table -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="8" border="0" width="100%" style="margin-bottom: 20px; border-bottom: 2px solid #999999;">
|
||||
<thead>
|
||||
<tr style="background-color: #f8f8f8;">
|
||||
<th style="text-align: left; font-weight: bold; font-size: 12px; padding: 10px 8px; border-bottom: 1px solid #999999;">Item code</th>
|
||||
<th style="text-align: left; font-weight: bold; font-size: 12px; padding: 10px 8px; border-bottom: 1px solid #999999;">Description</th>
|
||||
<th style="text-align: center; font-weight: bold; font-size: 12px; padding: 10px 8px; border-bottom: 1px solid #999999;">Quantity</th>
|
||||
<th style="text-align: right; font-weight: bold; font-size: 12px; padding: 10px 8px; border-bottom: 1px solid #999999;">Price</th>
|
||||
<th style="text-align: right; font-weight: bold; font-size: 12px; padding: 10px 8px; border-bottom: 1px solid #999999;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
foreach ($items as $item) {
|
||||
$line_total = $item['price'] * $item['quantity'];
|
||||
$message .= '<tr>
|
||||
<td style="padding: 10px 8px; border-bottom: 1px solid #dddddd; font-size: 13px;">SOFTWARE</td>
|
||||
<td style="padding: 10px 8px; border-bottom: 1px solid #dddddd; font-size: 13px;">' . htmlspecialchars($item['name']);
|
||||
|
||||
if ($item['serial_number'] !== 'N/A') {
|
||||
$message .= '<br><span style="font-size: 12px; color: #666666;">Serial: ' . htmlspecialchars($item['serial_number']) . '</span>';
|
||||
}
|
||||
if ($item['license_key'] !== 'Pending') {
|
||||
$message .= '<br><span style="font-size: 12px; color: #666666;">License: ' . htmlspecialchars($item['license_key']) . '</span>';
|
||||
}
|
||||
|
||||
$message .= '</td>
|
||||
<td style="text-align: center; padding: 10px 8px; border-bottom: 1px solid #dddddd; font-size: 13px;">' . htmlspecialchars($item['quantity']) . '</td>
|
||||
<td style="text-align: right; padding: 10px 8px; border-bottom: 1px solid #dddddd; font-size: 13px;">€ ' . number_format($item['price'], 2) . '</td>
|
||||
<td style="text-align: right; padding: 10px 8px; border-bottom: 1px solid #dddddd; font-size: 13px;">€ ' . number_format($line_total, 2) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$message .= '</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Totals -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="5" border="0" width="250" align="right" style="margin-top: 10px; font-size: 13px;">
|
||||
<tr>
|
||||
<td style="text-align: left; padding: 5px 0;">' . htmlspecialchars($lbl_subtotal) . '</td>
|
||||
<td style="text-align: right; padding: 5px 0;">€ ' . number_format($subtotal, 2) . '</td>
|
||||
</tr>';
|
||||
|
||||
if ($tax_amount > 0) {
|
||||
$message .= '<tr>
|
||||
<td style="text-align: left; padding: 5px 0;">' . htmlspecialchars($lbl_tax) . '</td>
|
||||
<td style="text-align: right; padding: 5px 0;">€ ' . number_format($tax_amount, 2) . '</td>
|
||||
</tr>';
|
||||
} else {
|
||||
$message .= '<tr>
|
||||
<td style="text-align: left; padding: 5px 0;">VAT</td>
|
||||
<td style="text-align: right; padding: 5px 0;">included</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$message .= '<tr style="border-top: 2px solid #000000;">
|
||||
<td style="text-align: left; padding: 8px 0 5px 0;"><strong>' . htmlspecialchars($lbl_total) . '</strong></td>
|
||||
<td style="text-align: right; padding: 8px 0 5px 0;"><strong>€ ' . number_format($payment_amount, 2) . '</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>';
|
||||
329
assets/mail/pdf_template_invoice.php
Normal file
329
assets/mail/pdf_template_invoice.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
$payment_method = 'payment_method_'.$invoice_data['header']['payment_method'];
|
||||
|
||||
$pdf = '<!DOCTYPE html>
|
||||
<html lang="' . strtolower($language) . '">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>' . htmlspecialchars($lbl_invoice) . ' - Total Safety Solutions</title>
|
||||
<style>
|
||||
@page {margin: 220px 50px; }
|
||||
|
||||
body {
|
||||
font-family: "DejaVu Sans", Arial, sans-serif;
|
||||
color: #000;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
margin: 10px auto;
|
||||
padding: 0;
|
||||
width:90%;
|
||||
}
|
||||
|
||||
#header {
|
||||
position: fixed;
|
||||
left: -52px;
|
||||
top: -220px;
|
||||
right: -50px;
|
||||
height: 200px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#header img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#footer {
|
||||
position: fixed;
|
||||
left: -50px;
|
||||
bottom: -250px;
|
||||
right: -50px;
|
||||
height: 150px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#footer img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invoice-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #2c5f5d;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.company-header {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.company-info, .contact-details {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.company-info h3 {
|
||||
font-weight: bold;
|
||||
margin: 0 0 5px 0;
|
||||
color: #2c5f5d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.company-info p, .contact-details p {
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.contact-details h3 {
|
||||
font-weight: bold;
|
||||
margin: 0 0 5px 0;
|
||||
color: #2c5f5d;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.invoice-details {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.invoice-left, .invoice-right {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.invoice-right {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
margin-bottom: 2px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
display: table-cell;
|
||||
font-weight: normal;
|
||||
width: 140px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
padding: 8px 6px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid #999;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.items-table .qty-col {
|
||||
text-align: center;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.items-table .price-col, .items-table .total-col {
|
||||
text-align: right;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.items-table th.qty-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.items-table th.price-col, .items-table th.total-col {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.totals-section {
|
||||
float: right;
|
||||
width: 250px;
|
||||
margin-top: 20px;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.total-label {
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.total-amount {
|
||||
display: table-cell;
|
||||
text-align: right;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.final-total {
|
||||
border-top: 1px solid #000;
|
||||
padding-top: 5px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.final-total .total-amount {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<img src="https://'.$portalURL.'/assets/images/TSS_invoice_header.png" alt="Invoice header">
|
||||
</div>
|
||||
<div id="footer">
|
||||
<img src="https://'.$portalURL.'/assets/images/TSS_invoice_footer.png" alt="Invoice footer">
|
||||
</div>
|
||||
|
||||
<div class="invoice-title">' . htmlspecialchars($lbl_invoice) . '</div>
|
||||
|
||||
<div class="company-header">
|
||||
<div class="company-info">
|
||||
<h3>Total Safety Solutions B.V.</h3>
|
||||
<p>Laarakkerweg 8</p>
|
||||
<p>5061 JR OISTERWIJK</p>
|
||||
<p>Nederland</p>
|
||||
</div>
|
||||
<div class="contact-details">
|
||||
<h3>contact-details</h3>
|
||||
<p>Ralf Adams</p>
|
||||
<p>+31 13 8221480</p>
|
||||
<p>ralfadams@totalsafetysolutions.nl</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="company-header">
|
||||
<div class="company-info">
|
||||
<h3>Customer</h3>
|
||||
<p>'.$invoice_data['customer']['name'].'</p>
|
||||
<p>'.$invoice_data['customer']['street'].'</p>
|
||||
<p>'.$invoice_data['customer']['zip'].', '.$invoice_data['customer']['city'].'</p>
|
||||
<p>'.$invoice_data['customer']['country'].'</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="invoice-details">
|
||||
<div class="invoice-left">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Invoice Date</div>
|
||||
<div class="detail-value">: ' . htmlspecialchars(date('d-m-Y', strtotime($invoice_date))) . '</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Invoice Number</div>
|
||||
<div class="detail-value">: ' . htmlspecialchars($order_id) . '</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Your Vat Number</div>
|
||||
<div class="detail-value">: ' . htmlspecialchars($invoice_data['customer']['vat_number'] ?? '') . '</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-right">
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Reference</div>
|
||||
<div class="detail-value">: Online order</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Order number</div>
|
||||
<div class="detail-value">: ' . htmlspecialchars($order_id) . '</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">Payment Method</div>
|
||||
<div class="detail-value">: ' . (${$payment_method} ?? $invoice_data['header']['payment_method'] ). '</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<th>Description</th>
|
||||
<th class="qty-col">Quantity</th>
|
||||
<th class="price-col">Price</th>
|
||||
<th class="total-col">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
foreach ($items as $item) {
|
||||
$line_total = $item['price'] * $item['quantity'];
|
||||
$pdf .= '<tr>
|
||||
<td>SOFTWARE</td>
|
||||
<td>' . htmlspecialchars($item['name']);
|
||||
|
||||
if ($item['serial_number'] !== 'N/A') {
|
||||
$pdf .= '<br><small>Serial: ' . htmlspecialchars($item['serial_number']) . '</small>';
|
||||
}
|
||||
if ($item['license_key'] !== 'Pending') {
|
||||
$pdf .= '<br><small>License: ' . htmlspecialchars($item['license_key']) . '</small>';
|
||||
}
|
||||
|
||||
$pdf .= '</td>
|
||||
<td class="qty-col">' . htmlspecialchars($item['quantity']) . ' </td>
|
||||
<td class="price-col">€ ' . number_format($item['price'], 2) . '</td>
|
||||
<td class="total-col">€ ' . number_format($line_total, 2) . '</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
$pdf .= '</tbody>
|
||||
</table>
|
||||
|
||||
<div class="totals-section">
|
||||
<div class="total-row">
|
||||
<div class="total-label">' . htmlspecialchars($lbl_subtotal) . '</div>
|
||||
<div class="total-amount">€ ' . number_format($subtotal, 2) . '</div>
|
||||
</div>';
|
||||
|
||||
if ($tax_amount > 0) {
|
||||
$pdf .= '<div class="total-row">
|
||||
<div class="total-label">' . htmlspecialchars($lbl_tax) . '</div>
|
||||
<div class="total-amount">€ ' . number_format($tax_amount, 2) . '</div>
|
||||
</div>';
|
||||
} else {
|
||||
$pdf .= '<div class="total-row">
|
||||
<div class="total-label">VAT</div>
|
||||
<div class="total-amount">included</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$pdf .= '<div class="total-row final-total">
|
||||
<div class="total-label">' . htmlspecialchars($lbl_total) . '</div>
|
||||
<div class="total-amount">€ ' . number_format($payment_amount, 2) . '</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>';
|
||||
1354
assets/marketing.js
Normal file
1354
assets/marketing.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
assets/mollie/.DS_Store
vendored
BIN
assets/mollie/.DS_Store
vendored
Binary file not shown.
BIN
assets/mollie/src/.DS_Store
vendored
BIN
assets/mollie/src/.DS_Store
vendored
Binary file not shown.
@@ -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.");
|
||||
|
||||
@@ -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 `<option value="">${typeof TRANS_COUNTRY !== 'undefined' ? TRANS_COUNTRY : 'Country'}</option>`;
|
||||
}
|
||||
|
||||
// Sort countries alphabetically
|
||||
const sortedCountries = Object.values(COUNTRIES).sort((a, b) => {
|
||||
return a.country.localeCompare(b.country);
|
||||
});
|
||||
|
||||
let options = '<option value="">Select country</option>';
|
||||
sortedCountries.forEach(data => {
|
||||
const selected = (selectedCountry === data.country) ? 'selected' : '';
|
||||
options += `<option value="${data.country}" ${selected}>${data.country}</option>`;
|
||||
});
|
||||
|
||||
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 = `
|
||||
<i class="fa-solid fa-exclamation-triangle" style="font-size: 24px;"></i>
|
||||
<div style="flex: 1;">
|
||||
<strong style="display: block; margin-bottom: 5px; font-size: 16px;">Browser Not Supported</strong>
|
||||
<p style="margin: 0; font-size: 14px; opacity: 0.95;">
|
||||
Please use <strong>Chrome</strong>, <strong>Edge</strong>, or <strong>Opera</strong> to access device connectivity features.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<i class="fa-solid fa-check-circle"></i> 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 = '<i class="fa-solid fa-handshake" style="margin-right: 8px;"></i>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 += `<div style="font-weight: 600; margin-bottom: 8px; color: #1565c0;"><i class="fa-solid fa-building" style="margin-right: 8px; width: 16px;"></i>${dealerInfo.name}</div>`;
|
||||
}
|
||||
if (dealerInfo.address || dealerInfo.city || dealerInfo.postalcode || dealerInfo.country) {
|
||||
dealerHtml += `<div style="margin-bottom: 6px; color: #666; display: flex; align-items: flex-start;"><i class="fa-solid fa-location-dot" style="margin-right: 8px; width: 16px; color: #999; margin-top: 2px;"></i><div>`;
|
||||
if (dealerInfo.address) {
|
||||
dealerHtml += `<div>${dealerInfo.address}</div>`;
|
||||
}
|
||||
if (dealerInfo.postalcode || dealerInfo.city) {
|
||||
dealerHtml += `<div>${[dealerInfo.postalcode, dealerInfo.city].filter(Boolean).join(' ')}</div>`;
|
||||
}
|
||||
if (dealerInfo.country) {
|
||||
dealerHtml += `<div>${dealerInfo.country}</div>`;
|
||||
}
|
||||
dealerHtml += `</div></div>`;
|
||||
}
|
||||
if (dealerInfo.email) {
|
||||
dealerHtml += `<div style="margin-bottom: 6px;"><i class="fa-solid fa-envelope" style="margin-right: 8px; width: 16px; color: #999;"></i><a href="mailto:${dealerInfo.email}" style="color: #1565c0; text-decoration: none;">${dealerInfo.email}</a></div>`;
|
||||
}
|
||||
if (dealerInfo.phone) {
|
||||
dealerHtml += `<div><i class="fa-solid fa-phone" style="margin-right: 8px; width: 16px; color: #999;"></i><a href="tel:${dealerInfo.phone}" style="color: #1565c0; text-decoration: none;">${dealerInfo.phone}</a></div>`;
|
||||
}
|
||||
|
||||
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 = '<i class="fa-solid fa-check-circle"></i> INSTALLED';
|
||||
} else {
|
||||
priceText.innerHTML = isFree
|
||||
? 'Free'
|
||||
: `${option.currency || "€"} ${price.toFixed(2)} <small style="font-size: 12px; font-weight: 400; color: #888;">(excl. VAT)</small>`;
|
||||
}
|
||||
|
||||
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 = '<i class="fa-solid fa-check"></i> Currently Installed';
|
||||
actionBtn.disabled = true;
|
||||
} else if (isFree) {
|
||||
actionBtn.innerHTML = '<i class="fa-solid fa-download"></i>';
|
||||
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 = '<i class="fa-solid fa-shopping-cart"></i>';
|
||||
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 = '<i class="fa-solid fa-check"></i> Currently Installed';
|
||||
actionBtn.disabled = true;
|
||||
} else if (isFree) {
|
||||
actionBtn.innerHTML = '<i class="fa-solid fa-download"></i>';
|
||||
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 = '<i class="fa-solid fa-shopping-cart"></i>';
|
||||
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 = `
|
||||
<div style="padding: 30px; border-bottom: 2px solid #e0e0e0;">
|
||||
<h3 style="margin: 0; color: #333; font-size: 24px;">${typeof TRANS_USER_INFO_REQUIRED !== 'undefined' ? TRANS_USER_INFO_REQUIRED : 'User Information Required'}</h3>
|
||||
<p style="margin: 10px 0 0 0; color: #666; font-size: 14px;">${typeof TRANS_USER_INFO_DESCRIPTION !== 'undefined' ? TRANS_USER_INFO_DESCRIPTION : 'Please provide your information to continue with software updates'}</p>
|
||||
</div>
|
||||
<div style="padding: 30px;">
|
||||
<form id="userInfoForm" style="display: flex; flex-direction: column; gap: 15px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 600;">${typeof TRANS_NAME !== 'undefined' ? TRANS_NAME : 'Name'} *</label>
|
||||
<input type="text" name="name" id="userInfoName" required style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 600;">${typeof TRANS_EMAIL !== 'undefined' ? TRANS_EMAIL : 'Email'} *</label>
|
||||
<input type="email" name="email" id="userInfoEmail" required style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 600;">${typeof TRANS_ADDRESS !== 'undefined' ? TRANS_ADDRESS : 'Address'} *</label>
|
||||
<input type="text" name="address" id="userInfoAddress" required placeholder="Street and number" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; margin-bottom: 10px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
||||
<input type="text" name="city" id="userInfoCity" required placeholder="${typeof TRANS_CITY !== 'undefined' ? TRANS_CITY : 'City'}" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; margin-bottom: 10px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<input type="text" name="postal" id="userInfoPostal" required placeholder="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
||||
<select name="country" id="userInfoCountry" required style="padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; transition: border 0.3s;" onfocus="this.style.borderColor='#04AA6D'" onblur="this.style.borderColor='#ddd'">
|
||||
${generateCountryOptions()}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
||||
<button type="button" onclick="location.reload()" style="padding: 15px 20px; background: #6c757d; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.background='#5a6268'" onmouseout="this.style.background='#6c757d'">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<button type="submit" style="flex: 1; padding: 15px; background: linear-gradient(135deg, #04AA6D 0%, #038f5a 100%); color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.3s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(4,170,109,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||||
${typeof TRANS_CONTINUE !== 'undefined' ? TRANS_CONTINUE : 'Continue'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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) {
|
||||
|
||||
<form id="freeInstallForm" style="display: flex; flex-direction: column; gap: 15px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Name *</label>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_NAME !== 'undefined' ? TRANS_NAME : 'Name'} *</label>
|
||||
<input type="text" name="name" id="freeInstallName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Email *</label>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_EMAIL !== 'undefined' ? TRANS_EMAIL : 'Email'} *</label>
|
||||
<input type="email" name="email" id="freeInstallEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Address *</label>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_ADDRESS !== 'undefined' ? TRANS_ADDRESS : 'Address'} *</label>
|
||||
<input type="text" name="address" id="freeInstallAddress" required placeholder="Street and number" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
||||
<input type="text" name="city" id="freeInstallCity" required placeholder="City" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
||||
<input type="text" name="city" id="freeInstallCity" required placeholder="${typeof TRANS_CITY !== 'undefined' ? TRANS_CITY : 'City'}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<input type="text" name="postal" id="freeInstallPostal" required placeholder="Postal code" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<input type="text" name="country" id="freeInstallCountry" required placeholder="Country" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<input type="text" name="postal" id="freeInstallPostal" required placeholder="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<select name="country" id="freeInstallCountry" required style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
${generateCountryOptions()}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -786,6 +1211,17 @@ function showPaymentModal(option) {
|
||||
const price = parseFloat(option.price || 0);
|
||||
const currency = option.currency || "€";
|
||||
|
||||
// Format description as bullet points
|
||||
const formatDescription = (desc) => {
|
||||
if (!desc) return '';
|
||||
// Split by bullet points or newlines and filter out empty lines
|
||||
const lines = desc.split(/[•·\n]/).map(line => line.trim()).filter(line => line.length > 0);
|
||||
if (lines.length <= 1) return desc; // Return as-is if no multiple lines
|
||||
return '<ul style="margin: 0; padding-left: 20px; color: #666; font-size: 13px; line-height: 1.6;">' +
|
||||
lines.map(line => `<li>${line}</li>`).join('') +
|
||||
'</ul>';
|
||||
};
|
||||
|
||||
// Create modal overlay
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "paymentModal";
|
||||
@@ -823,40 +1259,58 @@ function showPaymentModal(option) {
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #333;">${option.name || "Software Update"}</h4>
|
||||
<p style="margin: 0 0 5px 0; color: #666;">Version: <strong>${option.version || "N/A"}</strong></p>
|
||||
<p style="margin: 0 0 15px 0; color: #666;">${option.description || ""}</p>
|
||||
<div style="font-size: 24px; font-weight: bold; color: #04AA6D;">
|
||||
${currency} ${price.toFixed(2)}
|
||||
<div style="margin: 0 0 15px 0;">${formatDescription(option.description)}</div>
|
||||
<div id="priceDisplay" style="font-size: 14px; color: #666;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
||||
<span>Price (excl. VAT):</span>
|
||||
<span style="font-weight: 600;">${currency} ${price.toFixed(2)}</span>
|
||||
</div>
|
||||
<div id="taxDisplay" style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
||||
<span>VAT:</span>
|
||||
<span style="font-weight: 600;">-</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; padding-top: 10px; border-top: 2px solid #ddd; margin-top: 10px;">
|
||||
<span style="font-weight: bold;">Total:</span>
|
||||
<span id="totalDisplay" style="font-size: 24px; font-weight: bold; color: #04AA6D;">${currency} ${price.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="paymentForm" style="display: flex; flex-direction: column; gap: 15px;">
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Name *</label>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_NAME !== 'undefined' ? TRANS_NAME : 'Name'} *</label>
|
||||
<input type="text" name="name" id="paymentName" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Email *</label>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_EMAIL !== 'undefined' ? TRANS_EMAIL : 'Email'} *</label>
|
||||
<input type="email" name="email" id="paymentEmail" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Address *</label>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">${typeof TRANS_ADDRESS !== 'undefined' ? TRANS_ADDRESS : 'Address'} *</label>
|
||||
<input type="text" name="address" id="paymentAddress" required placeholder="Street and number" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
||||
<input type="text" name="city" id="paymentCity" required placeholder="City" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
||||
<input type="text" name="city" id="paymentCity" required placeholder="${typeof TRANS_CITY !== 'undefined' ? TRANS_CITY : 'City'}" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<input type="text" name="postal" id="paymentPostal" required placeholder="Postal code" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<input type="text" name="country" id="paymentCountry" required placeholder="Country" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<input type="text" name="postal" id="paymentPostal" required placeholder="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<select name="country" id="paymentCountry" required style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
${generateCountryOptions()}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">VAT Number</label>
|
||||
<input type="text" name="vat_number" id="paymentVatNumber" placeholder="Optional" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Payment Method *</label>
|
||||
<select name="payment_method" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||
<option value="">Select payment method</option>
|
||||
<option value="credit_card">Credit Card</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
<option value="bank_transfer">Bank Transfer</option>
|
||||
${typeof MOLLIE_ENABLED !== 'undefined' && MOLLIE_ENABLED ? '<option value="1">Credit Card</option>' : ''}
|
||||
${typeof PAYPAL_ENABLED !== 'undefined' && PAYPAL_ENABLED ? '<option value="3">PayPal</option>' : ''}
|
||||
${typeof PAY_ON_DELIVERY_ENABLED !== 'undefined' && PAY_ON_DELIVERY_ENABLED ? '<option value="bank_transfer">Bank Transfer</option>' : ''}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -875,6 +1329,45 @@ function showPaymentModal(option) {
|
||||
modal.appendChild(modalContent);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Function to calculate and update tax
|
||||
function updateTaxDisplay() {
|
||||
const selectedCountry = document.getElementById("paymentCountry").value;
|
||||
let taxRate = 0;
|
||||
|
||||
if (selectedCountry && typeof COUNTRIES !== 'undefined' && COUNTRIES) {
|
||||
const countryData = Object.values(COUNTRIES).find(c => c.country === selectedCountry);
|
||||
if (countryData) {
|
||||
taxRate = parseFloat(countryData.taxes) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const taxAmount = price * (taxRate / 100);
|
||||
const totalAmount = price + taxAmount;
|
||||
|
||||
// Update display
|
||||
const taxDisplay = document.getElementById("taxDisplay");
|
||||
const totalDisplay = document.getElementById("totalDisplay");
|
||||
|
||||
if (taxRate > 0) {
|
||||
taxDisplay.innerHTML = `
|
||||
<span>VAT (${taxRate}%):</span>
|
||||
<span style="font-weight: 600;">${currency} ${taxAmount.toFixed(2)}</span>
|
||||
`;
|
||||
} else {
|
||||
taxDisplay.innerHTML = `
|
||||
<span>VAT:</span>
|
||||
<span style="font-weight: 600;">-</span>
|
||||
`;
|
||||
}
|
||||
|
||||
totalDisplay.textContent = `${currency} ${totalAmount.toFixed(2)}`;
|
||||
|
||||
// Store tax info for form submission
|
||||
modal.taxRate = taxRate;
|
||||
modal.taxAmount = taxAmount;
|
||||
modal.totalAmount = totalAmount;
|
||||
}
|
||||
|
||||
// Prefill form with customer data from sessionStorage if available
|
||||
const savedCustomerData = sessionStorage.getItem('customerData');
|
||||
if (savedCustomerData) {
|
||||
@@ -885,12 +1378,19 @@ function showPaymentModal(option) {
|
||||
if (customerData.address) document.getElementById("paymentAddress").value = customerData.address;
|
||||
if (customerData.city) document.getElementById("paymentCity").value = customerData.city;
|
||||
if (customerData.postal) document.getElementById("paymentPostal").value = customerData.postal;
|
||||
if (customerData.country) document.getElementById("paymentCountry").value = customerData.country;
|
||||
if (customerData.vat_number) document.getElementById("paymentVatNumber").value = customerData.vat_number;
|
||||
if (customerData.country) {
|
||||
document.getElementById("paymentCountry").value = customerData.country;
|
||||
updateTaxDisplay(); // Calculate tax based on saved country
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error parsing saved customer data:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener to country select to update tax
|
||||
document.getElementById("paymentCountry").addEventListener('change', updateTaxDisplay);
|
||||
|
||||
// Close modal on cancel
|
||||
document.getElementById("cancelPayment").onclick = () => {
|
||||
document.body.removeChild(modal);
|
||||
@@ -900,6 +1400,16 @@ function showPaymentModal(option) {
|
||||
document.getElementById("paymentForm").onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const paymentMethod = formData.get("payment_method");
|
||||
|
||||
// Auto-determine payment provider based on payment method
|
||||
let paymentProvider = 'mollie'; // default
|
||||
if (paymentMethod === '3') { // PayPal payment method ID
|
||||
paymentProvider = 'paypal';
|
||||
} else if (paymentMethod === '1' || paymentMethod === 'bank_transfer') { // Mollie (Credit Card) or Bank Transfer
|
||||
paymentProvider = 'mollie';
|
||||
}
|
||||
|
||||
const paymentData = {
|
||||
name: formData.get("name"),
|
||||
email: formData.get("email"),
|
||||
@@ -907,9 +1417,13 @@ function showPaymentModal(option) {
|
||||
city: formData.get("city"),
|
||||
postal: formData.get("postal"),
|
||||
country: formData.get("country"),
|
||||
payment_method: formData.get("payment_method"),
|
||||
vat_number: formData.get("vat_number") || '',
|
||||
payment_method: paymentMethod,
|
||||
payment_provider: paymentProvider,
|
||||
version_id: option.version_id,
|
||||
price: price,
|
||||
item_price: price, // Price without VAT
|
||||
tax_amount: modal.taxAmount || 0, // Tax amount
|
||||
payment_amount: modal.totalAmount || price, // Total price including tax
|
||||
currency: currency
|
||||
};
|
||||
|
||||
@@ -920,7 +1434,8 @@ function showPaymentModal(option) {
|
||||
address: paymentData.address,
|
||||
city: paymentData.city,
|
||||
postal: paymentData.postal,
|
||||
country: paymentData.country
|
||||
country: paymentData.country,
|
||||
vat_number: paymentData.vat_number
|
||||
}));
|
||||
|
||||
await processPayment(paymentData, option, modal);
|
||||
@@ -1004,7 +1519,9 @@ async function processPayment(paymentData, option, modal) {
|
||||
const paymentRequest = {
|
||||
serial_number: deviceSerialNumber,
|
||||
version_id: option.version_id,
|
||||
user_data: paymentData // name, email, address only
|
||||
payment_method: paymentData.payment_method,
|
||||
payment_provider: paymentData.payment_provider,
|
||||
user_data: paymentData // name, email, address, etc.
|
||||
};
|
||||
|
||||
// Debug logging
|
||||
@@ -1012,13 +1529,15 @@ async function processPayment(paymentData, option, modal) {
|
||||
console.log("=== DEBUG: Payment Request ===");
|
||||
console.log("Serial Number:", deviceSerialNumber);
|
||||
console.log("Version ID:", option.version_id);
|
||||
console.log("Payment Method:", paymentData.payment_method);
|
||||
console.log("Payment Provider:", paymentData.payment_provider);
|
||||
console.log("User Data:", paymentData);
|
||||
console.log("Request payload:", paymentRequest);
|
||||
}
|
||||
|
||||
await logCommunication(`Payment initiated for version ${option.version_id}`, 'sent');
|
||||
await logCommunication(`Payment initiated for version ${option.version_id} via ${paymentData.payment_provider}`, 'sent');
|
||||
|
||||
// Call payment API to create Mollie payment
|
||||
// Call payment API (handles both Mollie and PayPal)
|
||||
const response = await fetch(link + "/v2/payment", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -1046,16 +1565,16 @@ async function processPayment(paymentData, option, modal) {
|
||||
}
|
||||
|
||||
if (result.checkout_url) {
|
||||
await logCommunication(`Redirecting to Mollie payment: ${result.payment_id}`, 'sent');
|
||||
await logCommunication(`Redirecting to ${paymentData.payment_provider} payment: ${result.payment_id}`, 'sent');
|
||||
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
console.log("DEBUG: Redirecting to Mollie checkout...");
|
||||
console.log(`DEBUG: Redirecting to ${paymentData.payment_provider} checkout...`);
|
||||
}
|
||||
|
||||
// Close modal before redirect
|
||||
document.body.removeChild(modal);
|
||||
|
||||
// Redirect to Mollie checkout page
|
||||
// Redirect to payment checkout page
|
||||
window.location.href = result.checkout_url;
|
||||
} else {
|
||||
throw new Error(result.error || "No checkout URL received");
|
||||
@@ -1175,7 +1694,7 @@ async function downloadAndInstallSoftware(option, customerData = null) {
|
||||
window.upgraded_version = option.version || "";
|
||||
|
||||
// DEBUG MODE: Don't auto-trigger upload, let user manually test
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||
if (typeof DEBUG !== 'undefined' && DEBUG && typeof DEBUG_ID !== 'undefined' && DEBUG_ID) {
|
||||
// Show upload section and button for manual testing
|
||||
document.getElementById("uploadSection").style.display = "block";
|
||||
const uploadBtn = document.getElementById("uploadSoftware");
|
||||
@@ -1193,14 +1712,50 @@ async function downloadAndInstallSoftware(option, customerData = null) {
|
||||
console.log("Click the 'Install Software' button to test if upload.js can handle the file");
|
||||
alert("DEBUG MODE: Download complete!\n\nBlob size: " + blob.size + " bytes\n\nClick the 'Install Software' button to test upload.js");
|
||||
} else {
|
||||
// PRODUCTION MODE: Show upload button and automatically trigger
|
||||
// PRODUCTION MODE: Hide button and show installation in progress
|
||||
document.getElementById("uploadSection").style.display = "block";
|
||||
const uploadBtn = document.getElementById("uploadSoftware");
|
||||
uploadBtn.style.display = "block";
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.style.display = "none";
|
||||
|
||||
// Hide device version information during installation
|
||||
const softwareOptions = document.getElementById("softwareOptions");
|
||||
if (softwareOptions) {
|
||||
softwareOptions.style.display = "none";
|
||||
}
|
||||
|
||||
// Create installation status indicator
|
||||
const installStatus = document.createElement("div");
|
||||
installStatus.id = "installationStatus";
|
||||
installStatus.style.cssText = `
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
`;
|
||||
installStatus.innerHTML = `
|
||||
<i class="fa-solid fa-spinner fa-spin" style="font-size: 32px; color: #04AA6D; margin-bottom: 10px;"></i>
|
||||
<p style="margin: 0; font-size: 18px; font-weight: 600; color: #333;">Installing Software...</p>
|
||||
<p style="margin: 5px 0 0 0; color: #666; font-size: 14px;">Please keep your device connected and do not close this page</p>
|
||||
`;
|
||||
|
||||
// Insert status before the hidden upload section
|
||||
document.getElementById("uploadSection").parentNode.insertBefore(installStatus, document.getElementById("uploadSection"));
|
||||
|
||||
progressBar("60", "Ready to install, starting upload...", "#04AA6D");
|
||||
uploadBtn.click();
|
||||
progressBar("60", "Starting automatic installation...", "#04AA6D");
|
||||
|
||||
// Enable the upload button and automatically click it
|
||||
setTimeout(() => {
|
||||
uploadBtn.disabled = false;
|
||||
|
||||
// Start monitoring for completion
|
||||
if (typeof startUploadMonitoring === 'function') {
|
||||
startUploadMonitoring();
|
||||
}
|
||||
|
||||
uploadBtn.click();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user