feat: Enhance software tool with country selection and tax calculation

- Added a helper function to generate country select options in software tool.
- Updated user info modal and payment modal to use country dropdowns instead of text inputs.
- Implemented tax calculation based on selected country in payment modal.
- Improved software options loading behavior in debug mode.
- Enhanced description formatting in payment modal.
- Added log modal for equipment updates with a link to view logs.
- Introduced a new countries settings file with tax rates for various countries.
- Minor adjustments to various PHP files for better handling of equipment and payment processes.
This commit is contained in:
“VeLiTi”
2026-01-16 16:01:31 +01:00
parent 7aebb762d3
commit 3db13b9ebf
27 changed files with 652 additions and 114 deletions

BIN
assets/.DS_Store vendored

Binary file not shown.

View File

@@ -1264,8 +1264,10 @@ function ioServer($api_call, $data){
$http_status = curl_getinfo($curl) ?? '200';
curl_close($curl);
if(debug){
debuglog($date." - ioServer: URL=$url, HTTP Code=$http_status, Response=" . substr($resp, 0, 500) . (strlen($resp) > 500 ? '...' : ''));
if (debug) {
$resp_log = $date . " - ioServer: URL=$url, HTTP Code= ". ($http_status['http_code'] ?? 'unknown') . ", Response=" . substr($resp, 0, 500) . (strlen($resp) > 500 ? '...' : '');
debuglog(json_encode($resp_log));
}
//Check If errorcode is returned
@@ -1728,33 +1730,38 @@ function getPartnerID($str){
// overview Indicators
//------------------------------------------
function overviewIndicators($warranty, $service, $sw_version, $sw_version_latest){
include dirname(__FILE__,2).'/settings/settings_redirector.php';
include dirname(__FILE__,2).'/settings/systemfirmware.php';
$indicator ='';
//In warranty
if (!empty($warranty ) && $warranty > $warrantydate){
$indicator .= '<span class="dot" style="background-color: #13b368;">W</span>';
} else {
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">W</span>';
}
//Out of Service
if (!empty($service) && $service < $servicedate){
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">S</span>';
} else {
$indicator .= '<span class="dot" style="background-color: #13b368;">S</span>';
}
include dirname(__FILE__,2).'/settings/settings_redirector.php';
include dirname(__FILE__,2).'/settings/systemfirmware.php';
$indicator ='';
$current_date = date('Y-m-d');
//In warranty
if (!empty($warranty ) && $warranty >= $current_date){
$indicator .= '<span class="dot" style="background-color: #13b368;">W</span>';
} else {
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">W</span>';
}
//Out of Service
if (!empty($service) && $service >= $current_date){
$indicator .= '<span class="dot" style="background-color: #13b368;">S</span>';
} else {
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">S</span>';
}
//Firmware
if (isset($sw_version_latest)){
if($sw_version_latest == 1){
$indicator .= '<span class="dot" style="background-color: #13b368;">F</span>';
if($sw_version_latest == 1){
$indicator .= '<span class="dot" style="background-color: #13b368;">F</span>';
}
else {
if ($sw_version == ''){
$indicator .= '<span class="dot" style="background-color: #13b368;">F</span>';
} else {
if ($sw_version == ''){
$indicator .= '<span class="dot" style="background-color: #13b368;">F</span>';
} else {
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">F</span>';
}
$indicator .= '<span class="dot" style="background-color: #eb8a0d;">F</span>';
}
}
}
return $indicator;
@@ -1783,11 +1790,12 @@ function warrantyStatus($input){
}
$warranty_date_due ='<span class="status">Unknown</span>';
if (!empty($input) && $input < $warrantydate){
$warranty_date_due = '<span class="status warranty_outdated">'.$warranty_outdated_text.'</span>';
$current_date = date('Y-m-d');
if (!empty($input) && $input >= $current_date){
$warranty_date_due = '<span class="">'.$warranty_recent.' ('.$input.')</span>';
} else {
$warranty_date_due = '<span class="">'.$warranty_recent.' ('.date('Y-m-d', strtotime($input. ' + 365 days')).')</span>';
$warranty_date_due = '<span class="status warranty_outdated">'.$warranty_outdated_text.'</span>';
}
return $warranty_date_due;
@@ -1814,13 +1822,15 @@ function serviceStatus($input){
else {
include dirname(__FILE__,2).'/settings/translations/translations_US.php';
}
$current_date = date('Y-m-d');
$service_date_due ='<span class="status">Unknown</span>';
if (!empty($input) && $input < $servicedate){
$service_date_due = '<span class="status service_renewal">'.$service_renewal_text.'</span>';
if (!empty($input) && $input >= $current_date){
$service_date_due ='<span class="">'.$service_recent.' ('.$input.')</span>';
} else {
$service_date_due ='<span class="">'.$service_recent.' ('.date('Y-m-d', strtotime($input. ' + 365 days')).')</span>';
$service_date_due = '<span class="status service_renewal">'.$service_renewal_text.'</span>';
}
return $service_date_due;
@@ -2976,20 +2986,29 @@ function showlog($object,$objectID){
$stmt->execute([$object,$objectID]);
$changes = $stmt->fetchAll(PDO::FETCH_ASSOC);
$view = '<label for="productcode">Changelog</label>';
foreach($changes as $change){
$object_value = $change['object_value'];
//UPDATE TO HUMANREADABLE STATUS
if ($object == 'equipment' && $change['object_field'] == 'status'){
$object_text = 'status'.$change['object_value'].'_text';
$object_value = $$object_text;
$view = '<div class="reg-fields">';
if ($changes) {
foreach ($changes as $change) {
$object_value = $change['object_value'];
// Human-readable status
if ($object == 'equipment' && $change['object_field'] == 'status') {
$object_text = 'status' . $change['object_value'] . '_text';
if (isset($$object_text)) {
$object_value = $$object_text;
}
}
$entry = htmlspecialchars( $object_value . ' - ' . $change['created'] . ' - ' . $change['createdby']);
$view .= ' <div class="reg-field">
<label>'.$change['object_field'].'</label>
<p>'.$entry.'</p>
</div>';
}
} else {
$view .= '<div style="color:#888;font-size:13px;padding:8px;">No changelog entries found.</div>';
}
$view .= '<input id="name" type="text" value="'.$change['object_field'].' - '.$object_value.' - '.$change['created'].' - '.$change['createdby'].'" readonly>';
}
return $view;
$view .= '</div>';
return $view;
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -5577,4 +5596,48 @@ function updateSoftwareLatestFlags($pdo, $version_id, $hw_version) {
$stmt->execute([$version['rowID']]);
}
}
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Generate Countries File from Taxes API +++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++
function generateCountriesFile($token){
//API call to get all taxes
$api_url = '/v2/taxes';
$response = ioAPIv2($api_url, '', $token);
if(!empty($response)){
//decode the API response
$taxes = json_decode($response, true);
if(!empty($taxes) && is_array($taxes)){
//Build the countries array - id as key, with country name and tax rate
$countries = [];
foreach($taxes as $tax){
$countries[$tax['id']] = [
'country' => $tax['country'] ?? '',
'taxes' => $tax['rate'] ?? 0
];
}
//Generate PHP file content
$fileContent = "<?php\n";
$fileContent .= "// Auto-generated countries file from taxes API\n";
$fileContent .= "// Generated on: " . date('Y-m-d H:i:s') . "\n\n";
$fileContent .= "\$countries = [\n";
foreach($countries as $id => $data){
$fileContent .= " " . $id . " => ['country' => '" . addslashes($data['country']) . "', 'taxes' => " . $data['taxes'] . "],\n";
}
$fileContent .= "];\n";
//Write to settings/countries.php
$filePath = dirname(__FILE__, 2) . '/settings/countries.php';
$result = file_put_contents($filePath, $fileContent);
return ($result !== false);
}
}
return false;
}

Binary file not shown.

View File

@@ -10,6 +10,26 @@ 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;
@@ -528,8 +548,18 @@ async function fetchSoftwareOptions() {
document.getElementById("softwareOptionsContainer").style.display = "block";
progressBar("100", "Software options loaded", "#04AA6D");
// Show user info modal immediately
showUserInfoModal();
// Show user info modal immediately (skip in debug mode)
if (typeof DEBUG === 'undefined' || !DEBUG) {
showUserInfoModal();
} else {
// In 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');
@@ -665,7 +695,7 @@ function displaySoftwareOptions(options) {
} else {
priceText.innerHTML = isFree
? 'Free'
: `${option.currency || "€"} ${price.toFixed(2)}`;
: `${option.currency || "€"} ${price.toFixed(2)} <small style="font-size: 12px; font-weight: 400; color: #888;">(excl. VAT)</small>`;
}
priceSection.appendChild(priceText);
@@ -777,7 +807,9 @@ function showUserInfoModal() {
<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'">
<input type="text" name="country" id="userInfoCountry" required placeholder="${typeof TRANS_COUNTRY !== 'undefined' ? TRANS_COUNTRY : 'Country'}" 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>
@@ -967,7 +999,9 @@ function showFreeInstallModal(option) {
<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="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<input type="text" name="country" id="freeInstallCountry" required placeholder="${typeof TRANS_COUNTRY !== 'undefined' ? TRANS_COUNTRY : 'Country'}" 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>
@@ -1045,6 +1079,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";
@@ -1082,9 +1127,20 @@ 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>
@@ -1105,7 +1161,9 @@ function showPaymentModal(option) {
<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="${typeof TRANS_POSTAL !== 'undefined' ? TRANS_POSTAL : 'Postal code'}" style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
<input type="text" name="country" id="paymentCountry" required placeholder="${typeof TRANS_COUNTRY !== 'undefined' ? TRANS_COUNTRY : 'Country'}" 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>
@@ -1134,6 +1192,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) {
@@ -1144,12 +1241,18 @@ 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.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);
@@ -1160,15 +1263,15 @@ function showPaymentModal(option) {
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 === 'paypal') {
if (paymentMethod === '3') { // PayPal payment method ID
paymentProvider = 'paypal';
} else if (paymentMethod === 'credit_card' || paymentMethod === 'bank_transfer') {
} 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"),
@@ -1179,7 +1282,9 @@ function showPaymentModal(option) {
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
};