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

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
};