From 653e33d7e9678d32084554f244564730cfb8eeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CVeLiTi=E2=80=9D?= <“info@veliti.nl”> Date: Sun, 21 Dec 2025 14:16:55 +0100 Subject: [PATCH] Add software tool functionality with device connection and upgrade options - Implemented the software tool page with user interface for connecting devices. - Added functionality to display connection status and software upgrade options. - Included a help modal with step-by-step instructions for users. - Integrated error handling and user permission checks. - Enhanced user experience with dynamic content updates and visual feedback. --- PAYMENT_IMPLEMENTATION_SUMMARY.md | 103 ++ PAYMENT_INTEGRATION_PLAN.md | 825 +++++++++++++ api/v0/post/application.php | 5 +- api/v1/get/products_software.php | 7 +- api/v2/get/products_software.php | 7 +- api/v2/get/software_available.php | 188 ++- api/v2/get/software_download.php | 8 +- api/v2/get/software_update.php | 260 +++- api/v2/post/history.php | 5 +- .../post/products_software_upgrade_paths.php | 15 +- api/v2/post/products_software_versions.php | 5 +- assets/functions.php | 97 ++ assets/softwaretool.js | 1099 +++++++++++++++++ communication.php | 7 +- dealer.php | 16 +- equipment_data.php | 9 +- firmwaretool.php | 11 +- index.php | 8 +- language.php | 6 +- logfile.php | 4 +- maintenance.php | 12 +- partner.php | 11 +- products_configurations.php | 7 +- products_software.php | 6 +- products_software_upgrade_paths_manage.php | 79 +- products_software_version.php | 2 +- products_versions.php | 6 +- profile.php | 8 +- settings.php | 56 +- settings/settingsmenu.php | 14 +- settings/translations/translations_US.php | 16 + softwaretool.php | 162 +++ style/admin.css | 3 +- user.php | 17 +- 34 files changed, 2915 insertions(+), 169 deletions(-) create mode 100644 PAYMENT_IMPLEMENTATION_SUMMARY.md create mode 100644 PAYMENT_INTEGRATION_PLAN.md create mode 100644 assets/softwaretool.js create mode 100644 softwaretool.php diff --git a/PAYMENT_IMPLEMENTATION_SUMMARY.md b/PAYMENT_IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..579343f --- /dev/null +++ b/PAYMENT_IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,103 @@ +# Payment Integration Implementation Summary + +## Overview +Complete payment integration for software upgrades using existing ecommerce infrastructure (transaction API, invoice API, PHPMailer, DomPDF). + +## New Files to Create + +### 1. `/webhook_mollie.php` (Root directory) +**Purpose**: Mollie webhook handler specifically for software upgrades +**Based on**: Existing webhook.php from commerce product +**Key features**: +- ✅ Uses `/v2/transactions/` API for status updates (consistent with commerce) +- ✅ Uses `/v2/invoice/` API for invoice generation +- ✅ Creates PDF invoice with DomPDF +- ✅ Sends email via PHPMailer +- ✅ Creates software license +- ✅ Multi-language support +- ✅ Sends to bookkeeping if configured + +**Webhook URL**: `https://yourdomain.com/webhook_mollie.php` + +### 2. `/api/v2/post/payment.php` +**Purpose**: Create Mollie payment for software upgrade +**Input**: serial_number, version_id, user_data +**Output**: {checkout_url, payment_id} +**Security**: Server-side price calculation + +### 3. `/api/v2/get/payment.php` +**Purpose**: Retrieve payment status +**Input**: ?payment_id=xxx +**Output**: {payment_id, payment_status, serial_number, equipment_id, ...} + +## Modified Files + +### 1. `/assets/functions.php` +**Add new functions**: +- `generateUniqueLicenseKey()` - Generate unique license keys +- `generateSoftwareInvoice($invoice_data, $order_id, $language)` - Generate HTML invoice for software upgrades + - Based on existing `generateInvoice()` function + - Custom template for software licenses + - Shows: Device serial number, software version, license key, expiry date + - Returns: [$html_content, $customer_email, $order_id] + +### 2. `/assets/softwaretool.js` +**Modify**: +- `processPayment()` - Call `/v2/post/payment` API +- `downloadAndInstallSoftware()` - Add serial number verification + +### 3. `/softwaretool.php` +**Add**: Payment return detection (`?payment_id=xxx`) + +## Database +**No changes needed** - Uses existing: +- `transactions` table (txn_id, payment_status, payment_amount, etc.) +- `transactions_items` table (item_id, item_options with JSON) + +## Payment Status Codes (Matching Commerce System) +- `0` = Pending +- `1` = Paid +- `101` = Open/Pending (Mollie) +- `102` = Failed +- `103` = Expired +- `999` = Canceled + +## Implementation Order + +1. ✅ Add Mollie constants to config.php +2. Create helper functions in functions.php: + - `generateUniqueLicenseKey()` + - `generateSoftwareInvoice()` +3. Create `/api/v2/post/payment.php` +4. Create `/api/v2/get/payment.php` +5. Create `/webhook_mollie.php` +6. Modify frontend JavaScript +7. Modify softwaretool.php +8. Test in DEBUG mode +9. Test with Mollie sandbox +10. Deploy to production + +## Key Benefits + +1. **Consistent with ecommerce** - Uses same API structure +2. **Professional invoices** - PDF generation + email delivery +3. **Complete audit trail** - Transactions + invoices + licenses +4. **Multi-language** - Invoice language based on customer country +5. **Bookkeeping integration** - Auto-send to bookkeeping email +6. **Refund handling** - Webhook detects refunds (TODO: disable license) + +## Invoice Email Template + +The email will include: +- Subject: "Software Upgrade - Invoice: [ORDER_ID]" +- HTML body with invoice details +- PDF attachment (Invoice_[ORDER_ID].pdf) +- Sent to customer email + bookkeeping (if configured) + +**Invoice contains:** +- Customer details (name, address, email) +- Order ID / Transaction ID +- Software upgrade details (version, device serial number) +- License key + expiry date (2099-12-31) +- Price breakdown +- Company information diff --git a/PAYMENT_INTEGRATION_PLAN.md b/PAYMENT_INTEGRATION_PLAN.md new file mode 100644 index 0000000..8b672e9 --- /dev/null +++ b/PAYMENT_INTEGRATION_PLAN.md @@ -0,0 +1,825 @@ +# Plan: Payment Flow with Redirect for Software Upgrade Tool + +## User Request +Design the payment flow for software upgrades using Mollie (payment provider) with the following requirements: +1. User initiates paid upgrade +2. System redirects to Mollie for payment +3. After successful payment, Mollie redirects back to software tool +4. System creates license connected to serialnumber +5. Download and upload to device starts automatically + +## Key Challenge +**User Experience**: How to resume the upgrade flow after payment redirect, ensuring seamless transition from payment completion back to automatic download/upload. + +--- + +## Current System Analysis + +### Existing Infrastructure +✅ **Transactions Table** - Ready for payment tracking (txn_id, payment_status, payment_amount) +✅ **Licenses Table** - Has transaction_id field for linking (currently unused) +✅ **Payment Modal UI** - Frontend form exists in softwaretool.js (lines 455-572) +❌ **Payment Provider Integration** - No Mollie/Stripe/PayPal implementation exists +❌ **Webhook Handlers** - No callback endpoints implemented +❌ **Redirect Handling** - No return_url/cancel_url pattern +❌ **License Auto-creation** - No logic to create licenses after successful payment +❌ **Payment Session State** - No state persistence across redirect cycle + +### Current Payment Flow (Simulated) +``` +softwaretool.js: +1. User clicks "Purchase & Install" → showPaymentModal() +2. User fills form → processPayment() +3. [SIMULATED 2-second delay - no actual payment] +4. downloadAndInstallSoftware() → triggers upload.js +``` + +**Problem**: Step 3 will become a redirect to Mollie, breaking the flow and losing all state. + +--- + +## User's Preferred Flow (APPROVED) + +The user wants a simpler, more elegant approach: + +1. **Payment creates license** - Mollie webhook creates license linked to serial number +2. **Return to software tool** - User redirected back with upgrade information in URL +3. **Reconnect device** - User connects device (may be different device!) +4. **Re-check software options** - System calls `software_update` API again +5. **License automatically applied** - Paid upgrade now shows as FREE (license found) +6. **Install button changes** - "Purchase & Install" becomes "Install Now" (free) +7. **User proceeds** - Click install to download and upload + +### Key Benefits +- ✅ No complex state management needed +- ✅ Existing license checking logic handles everything +- ✅ User can connect different device (license is separate) +- ✅ Clean separation: payment → license → upgrade check +- ✅ Works with existing `software_update.php` license validation (lines 274-311) + +### Critical Security Check +**IMPORTANT**: Before starting upload, verify serial number matches the one from payment. +- Store `serial_number` in payment session/URL +- When user returns and reconnects device, compare: + - `serialnumber_from_payment` vs `serialnumber_from_device` +- If mismatch: Show warning "Different device detected - license applied to original device (SN: XXXXX)" + +--- + +## Proposed Solution Architecture + +### Database Changes +**No new tables needed** - Use existing `transactions` and `transactions_items` tables + +**`transactions` table fields:** +- `txn_id` (varchar 255, UNIQUE) - Store Mollie payment_id here +- `payment_status` (int 11) - Payment status code (need to define: 0=pending, 1=paid, 2=failed, 3=canceled, etc.) +- `payment_amount` (decimal 7,2) - Price +- `payer_email` (varchar 255) - Customer email +- `first_name`, `last_name` - Customer name +- `address_*` fields - Customer address +- `account_id` (varchar 255) - Can store serial_number here or user account +- `payment_method` (int 11) - Payment method ID +- `created`, `updated` - Timestamps + +**`transactions_items` table fields:** +- `txn_id` (varchar 255) - Links to transactions.txn_id +- `item_id` (int 11) - Store version_id (products_software_versions.rowID) +- `item_price` (decimal 7,2) - Software version price +- `item_quantity` (int 11) - Always 1 for software upgrades +- `item_options` (varchar 255) - Store JSON with: `{"serial_number": "22110095", "equipment_id": 123, "hw_version": "r08"}` +- `created`, `updated` - Timestamps + +**Payment Status Codes** (matching existing webhook.php): +- `0` = Pending (initial state, before Mollie call) +- `1` = Paid (payment successful) +- `101` = Open/Pending (Mollie isPending or isOpen) +- `102` = Failed (Mollie isFailed) +- `103` = Expired (Mollie isExpired) +- `999` = Canceled (Mollie isCanceled) + +### API Endpoints Needed (Following Standard Structure) +1. **POST /api/v2/post/payment.php** - Initiates Mollie payment (create action) +2. **GET /api/v2/get/payment.php** - Retrieves payment status and details +3. **NEW `webhook_mollie.php`** - Separate webhook for software upgrades (based on webhook.php structure, but simplified for this use case) + +### Simplified Flow Diagram +``` +[User] → [Select Paid Upgrade] → [Payment Modal] + ↓ + processPayment() calls POST /v2/post/payment + - Store pending payment in DB + - Call Mollie API: create payment + - Get checkout URL + - Redirect user to Mollie + ↓ + User pays at Mollie ←→ [Mollie Payment Page] + ↓ + ┌───────────────────────┴───────────────────────┐ + ↓ ↓ + [Mollie redirects user back] [Mollie webhook fires asynchronously] + softwaretool.php?payment_id={payment_id} NEW webhook_mollie.php receives POST + - Calls GET /v2/get/payment?payment_id=X - Fetches payment from Mollie API + - Shows status message - Updates transaction status (1=paid) + - Display device connection button - Creates license in products_software_licenses + - Updates equipment.sw_version_license + ↓ + [User clicks "Connect Device"] + ↓ + connectDeviceForSoftware() + - User connects device (may be different device!) + - Read SN, FW, HW from device + ↓ + checkSoftwareAvailability() → calls /v2/software_update + - Existing license validation (lines 274-311) finds license + - Paid upgrade now shows price = 0.00 + - Button text changes: "Purchase & Install" → "Install Now" + ↓ + [User clicks "Install Now"] + ↓ + selectUpgrade(option) → sees price = 0, skips payment modal + ↓ + downloadAndInstallSoftware() + - CRITICAL: Verify serial number matches payment + - If mismatch: Show warning but allow (license already applied) + - Download firmware + - Trigger upload.js +``` + +### Key Design Decisions + +**1. Leverage Existing License Logic** +- No need to manually check licenses in frontend +- `software_update.php` lines 274-311 already handle this perfectly +- When license exists and is valid, price automatically becomes 0.00 +- Frontend just needs to check `if (price === 0)` to show different button + +**2. Minimal State Management** +- Store only essential data in `transactions` and `transactions_items` +- URL parameters carry context back (payment_id) +- No need to persist entire upgrade state +- User reconnects device = fresh state from device + +**3. Serial Number Verification** +- Store `serial_number` in `transactions_items.item_options` JSON +- After return, when user reconnects device, compare: + - `serialnumber_from_payment` (from item_options JSON) + - `deviceSerialNumber` (from connected device) +- If mismatch: Show warning "Different device detected. License was applied to device SN: XXXXX" +- Allow upload to proceed (license is already created for original SN) + +**4. Separate Webhook for Software Upgrades** +- Create new `webhook_mollie.php` based on structure from existing webhook.php +- Specifically designed for software upgrade payments (no invoice generation needed) +- Simplified logic: Just update transaction status and create license +- Webhook URL: `https://site.com/webhook_mollie.php` +- Webhook is authoritative for license creation +- Return URL handler just shows status message +- Race condition safe: user may see "payment successful" before webhook fires + +--- + +## Implementation Plan + +### Phase 1: Database & Payment Infrastructure + +**1.1 Database Table - No Changes Needed** +``` +The existing transactions and transactions_items tables will be used. +No schema modifications required. +``` + +**1.2 Create `/api/v2/post/payment.php`** +```php + 0 (free upgrades shouldn't reach payment API) +4. Call Mollie API FIRST to get payment_id: + $mollie->payments->create([ + 'amount' => ['currency' => 'EUR', 'value' => $final_price], + 'description' => 'Software upgrade to version X', + 'redirectUrl' => 'https://site.com/softwaretool.php?payment_return=1&payment_id={payment_id}', + 'webhookUrl' => 'https://site.com/webhook_mollie.php', // NEW webhook for software upgrades + 'metadata' => ['order_id' => $mollie_payment_id] // for compatibility + ]) +5. Store transaction in DB with Mollie payment_id: + INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name, address_*, account_id, ...) + VALUES ($mollie_payment_id, $final_price, 0, ...) -- 0 = pending +6. Store transaction item: + INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, ...) + VALUES ($mollie_payment_id, $version_id, $final_price, 1, '{"serial_number":"...", "equipment_id":...}', ...) +7. Return JSON: {checkout_url: $mollie_checkout_url, payment_id: $mollie_payment_id} +``` + +**1.3 Create `/api/v2/get/payment.php`** +```php + clientID, "clientsecret" => clientsecret), JSON_UNESCAPED_UNICODE); +$responses = ioAPIv2('/v2/authorization', $data,''); +//Decode Payload +if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';} +$clientsecret = $responses['token']; + +//+++++++++++++++++++++++++++++++++++++++++++++++++++++ +// BASEURL is required for invoice template +//+++++++++++++++++++++++++++++++++++++++++++++++++++++ +$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/'; +define('base_url', $base_url); + +try { + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // Initialize the Mollie API library + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + require "initialize.php"; // Mollie initialization (from commerce webhook) + + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + //Retrieve the payment's current state + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + $payment = $mollie->payments->get($_POST["id"]); + $orderId = $payment->metadata->order_id; + + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // Update the transaction using existing API + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + + if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) { + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // PAID - Update transaction status via API + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + $transaction = json_decode($transaction,true); + + if ($transaction !== null && !empty($transaction)) { + if(count($transaction) > 0) { + + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + // CREATE LICENSE for software upgrade + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + $pdo = dbConnect($dbname); + + // Fetch transaction items to find software upgrade + $sql = 'SELECT * FROM transactions_items WHERE txn_id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$orderId]); + $items = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($items as $item) { + if (!empty($item['item_options'])) { + $options = json_decode($item['item_options'], true); + + // Check if this is a software upgrade (has serial_number and equipment_id) + if (isset($options['serial_number']) && isset($options['equipment_id'])) { + + // Check if license already exists for this transaction + $sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$orderId]); + $existing_license = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$existing_license) { + // Generate unique license key + $license_key = generateUniqueLicenseKey(); + + // Create license + $sql = 'INSERT INTO products_software_licenses + (license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + $license_key, + $options['equipment_id'], + 'upgrade', + 1, // active + date('Y-m-d H:i:s'), + '2099-12-31 23:59:59', // effectively permanent + $orderId, + date('Y-m-d H:i:s'), + 'webhook' // created by webhook + ]); + + // Update equipment.sw_version_license + $sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$license_key, $options['equipment_id']]); + + error_log("Webhook: License created for equipment_id: " . $options['equipment_id'] . ", license_key: " . $license_key); + } + } + } + } + + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + //Generate INVOICE RECORD via API + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + $payload = json_encode(array("txn_id" => $transaction['transaction_id']), JSON_UNESCAPED_UNICODE); + $invoice = ioAPIv2('/v2/invoice/',$payload,$clientsecret); + $invoice = json_decode($invoice,true); + + if ($invoice !== null && !empty($invoice)) { + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + //Generate INVOICE PDF and send email + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + $invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret); + $invoice_cust = json_decode($invoice_cust,true); + + // Determine invoice language + if (!empty($invoice_cust['customer']['language'])) { + $invoice_language = strtoupper($invoice_cust['customer']['language']); + } elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) { + $invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])]; + } else { + $invoice_language = 'US'; // Default fallback + } + + // Generate invoice HTML (using custom template for software upgrades) + list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language); + + //CREATE PDF using DomPDF + $dompdf->loadHtml($data); + $dompdf->setPaper('A4', 'portrait'); + $dompdf->render(); + $subject = ($invoice_software_subject ?? 'Software Upgrade - Invoice: ').$order_id; + $attachment = $dompdf->output(); + + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + //Send email via PHPMailer + //+++++++++++++++++++++++++++++++++++++++++++++++++++++ + send_mail_by_PHPMailer($customer_email, $subject, $data, $attachment, $subject); + + if(invoice_bookkeeping){ + send_mail_by_PHPMailer(email_bookkeeping, $subject, $data, $attachment, $subject); + } + } + } + } + + } elseif ($payment->isOpen()) { + // OPEN/PENDING (101) + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + + } elseif ($payment->isPending()) { + // PENDING (101) + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + + } elseif ($payment->isFailed()) { + // FAILED (102) + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 102), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + + } elseif ($payment->isExpired()) { + // EXPIRED (103) + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 103), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + + } elseif ($payment->isCanceled()) { + // CANCELED (999) + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 999), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + + } elseif ($payment->hasRefunds()) { + // REFUNDED (1 + refund flag) + $payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE); + $transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret); + // TODO: Disable license on refund + } + +} catch (\Mollie\Api\Exceptions\ApiException $e) { + error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage())); + http_response_code(500); + echo "API call failed: " . htmlspecialchars($e->getMessage()); +} catch (Exception $e) { + error_log("Webhook error: " . htmlspecialchars($e->getMessage())); + http_response_code(500); +} +``` + +**Key Features (matching commerce webhook.php):** +- ✅ Uses `/v2/transactions/` API for status updates +- ✅ Uses `/v2/invoice/` API for invoice generation +- ✅ Generates PDF invoice with DomPDF +- ✅ Sends email via PHPMailer +- ✅ Creates license for software upgrade +- ✅ Uses same payment status codes (0, 1, 101, 102, 103, 999) +- ✅ Handles refunds (TODO: disable license) +- ✅ Multi-language invoice support +- ✅ Sends to bookkeeping if configured + +### Phase 2: Frontend Integration + +**2.1 Modify `processPayment()` in softwaretool.js (lines 574-608)** +```javascript +async function processPayment(paymentData, option, modal) { + try { + progressBar("10", "Processing payment...", "#04AA6D"); + + // SECURITY: Only send serial_number and version_id + // Server will calculate the price to prevent tampering + const response = await fetch(link + "/v2/post/payment", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + document.getElementById("servicetoken").textContent + }, + body: JSON.stringify({ + serial_number: deviceSerialNumber, + version_id: option.version_id, + user_data: paymentData // name, email, address only + // REMOVED: price, currency - server calculates these + }) + }); + + const result = await response.json(); + + if (result.checkout_url) { + await logCommunication(`Redirecting to payment provider`, 'sent'); + // Redirect to Mollie checkout + window.location.href = result.checkout_url; + } else { + throw new Error(result.error || "Failed to create payment"); + } + + } catch (error) { + await logCommunication(`Payment error: ${error.message}`, 'error'); + progressBar("0", "Payment failed: " + error.message, "#ff6666"); + alert("Payment failed: " + error.message); + } +} +``` + +**2.2 Remove equipment_id tracking - NOT NEEDED** +```javascript +// SECURITY: We don't need to track equipment_id in frontend +// The server will look it up from serial_number in the payment/create API +// This prevents tampering with equipment_id +``` + +**2.3 Add Serial Number Verification in `downloadAndInstallSoftware()` (lines 610-699)** +```javascript +async function downloadAndInstallSoftware(option) { + // Check if we're returning from payment + const urlParams = new URLSearchParams(window.location.search); + const paymentId = urlParams.get('payment_id'); + + if (paymentId) { + // Verify serial number matches payment using GET /v2/get/payment + const response = await fetch(link + `/v2/get/payment?payment_id=${paymentId}`, { + method: "GET", + headers: { + "Authorization": "Bearer " + document.getElementById("servicetoken").textContent + } + }); + + const paymentData = await response.json(); + + if (paymentData.serial_number !== deviceSerialNumber) { + const confirmed = confirm( + `WARNING: Different device detected!\n\n` + + `License was created for device: ${paymentData.serial_number}\n` + + `Currently connected device: ${deviceSerialNumber}\n\n` + + `The license is already applied to the original device. ` + + `Do you want to continue with this device anyway?` + ); + + if (!confirmed) { + progressBar("0", "Upload canceled by user", "#ff6666"); + return; + } + } + } + + // Continue with existing download logic... + selectedSoftwareUrl = option.source; + // ... rest of function unchanged +} +``` + +**Note**: Serial number verification uses existing GET /v2/get/payment endpoint (no separate verify endpoint needed) + +### Phase 3: Return URL Handling + +**3.1 Modify `softwaretool.php` to detect return from payment** +```php +// Add near top of softwaretool.php (after includes, before $view) + +``` + +**3.2 Optional: Auto-trigger device connection after payment return** +```javascript +// In softwaretool.js, check URL on page load +window.addEventListener('DOMContentLoaded', function() { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has('payment_id')) { + // Show message: "Payment successful! Please reconnect your device." + // Optionally auto-show device connection UI + } +}); +``` + +### Phase 4: Testing Strategy + +**4.1 DEBUG Mode Testing (Complete Simulation)** +```php +// In /api/v2/post/payment.php, check if DEBUG mode +if (defined('debug') && debug) { + // FULL SIMULATION: No Mollie API connection, no device connection + $fake_payment_id = 'DEBUG_' . uniqid(); + + // 1. Store transaction with status 0 (pending) + $sql = 'INSERT INTO transactions + (txn_id, payment_amount, payment_status, payer_email, first_name, last_name, + address_street, address_city, address_state, address_zip, address_country, account_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + $fake_payment_id, + $final_price, + 0, // 0 = pending + $post_content['user_data']['email'], + $post_content['user_data']['first_name'] ?? '', + $post_content['user_data']['last_name'] ?? '', + $post_content['user_data']['address_street'] ?? '', + $post_content['user_data']['address_city'] ?? '', + $post_content['user_data']['address_state'] ?? '', + $post_content['user_data']['address_zip'] ?? '', + $post_content['user_data']['address_country'] ?? '', + $post_content['serial_number'] // store serial number in account_id + ]); + + // 2. Store transaction item + $item_options = json_encode([ + 'serial_number' => $post_content['serial_number'], + 'equipment_id' => $equipment_id, + 'hw_version' => $hw_version + ]); + $sql = 'INSERT INTO transactions_items + (txn_id, item_id, item_price, item_quantity, item_options) + VALUES (?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + $fake_payment_id, + $post_content['version_id'], + $final_price, + 1, + $item_options + ]); + + // 3. Immediately simulate webhook success (update status to paid + create license) + $sql = 'UPDATE transactions SET payment_status = 1 WHERE txn_id = ?'; // 1 = paid + $stmt = $pdo->prepare($sql); + $stmt->execute([$fake_payment_id]); + + // 4. Create license + $license_key = generateUniqueLicenseKey(); + $sql = 'INSERT INTO products_software_licenses + (license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; + $stmt = $pdo->prepare($sql); + $stmt->execute([ + $license_key, + $equipment_id, + 'upgrade', + 1, + date('Y-m-d H:i:s'), + '2099-12-31 23:59:59', + $fake_payment_id, + date('Y-m-d H:i:s'), + $username + ]); + + // 5. Update equipment.sw_version_license + $sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$license_key, $equipment_id]); + + // 6. Return fake checkout URL that redirects immediately + $messages = [ + 'checkout_url' => 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id=' . $fake_payment_id, + 'payment_id' => $fake_payment_id + ]; + echo json_encode($messages); + exit; +} +``` + +**Note**: In DEBUG mode, the entire payment + license creation flow is simulated without: +- Calling Mollie API +- Requiring physical device connection (works with DEBUG mode mock device data in softwaretool.js) + +**4.2 Mollie Sandbox Testing** +1. Use Mollie test API key +2. Test successful payment flow +3. Test failed payment flow +4. Test canceled payment flow +5. Test webhook delivery +6. Test license creation + +**4.3 Serial Number Mismatch Testing** +1. Complete payment with device A (SN: 22110095) +2. Disconnect device A +3. Connect device B (different SN) +4. Verify warning appears +5. Verify license was created for device A + +--- + +## Critical Files to Modify + +### New Files +- `/api/v2/post/payment.php` - Payment creation (POST) +- `/api/v2/get/payment.php` - Payment status retrieval (GET) +- `/webhook_mollie.php` - Mollie webhook handler for software upgrades (based on existing webhook.php structure) +- `generateSoftwareInvoice()` function in `/assets/functions.php` - Invoice template for software upgrades + +### Modified Files +- `/assets/softwaretool.js`: + - `processPayment()` (lines 574-608) - Call POST /v2/post/payment instead of simulation + - `downloadAndInstallSoftware()` (lines 610-699) - Add serial number verification using GET /v2/get/payment + - Add payment return detection on page load (optional) +- `/softwaretool.php`: + - Add payment return URL detection (check for ?payment_id=X) + - Optionally show success message banner after payment +- `/api/v2/get/software_update.php`: + - **No changes needed** (existing license logic at lines 274-311 works perfectly!) + +### Database & Helper Functions +- No new tables needed (using existing `transactions` and `transactions_items`) +- Add helper function `generateUniqueLicenseKey()` in `assets/functions.php` +- Payment status codes already defined in existing webhook.php (0, 1, 101, 102, 103, 999) + +--- + +## Security Architecture Summary + +### ✅ **SECURE APPROACH: Server-Side Price Validation** + +**Frontend sends:** +- `serial_number` (from connected device) +- `version_id` (which version they want) +- `user_data` (name, email, address) + +**Backend does:** +1. Look up equipment from `serial_number` +2. Look up version from `version_id` +3. **Calculate actual price using same logic as software_update.php**: + - Check upgrade path pricing (lines 244-260) + - Check if license exists and reduces price (lines 274-311) + - Get final server-calculated price +4. Verify price > 0 (reject free upgrades) +5. Create Mollie payment with **SERVER-CALCULATED price** +6. Store pending payment with correct price + +### ❌ **INSECURE APPROACH: Never Do This** +```javascript +// WRONG - User can modify price in browser console! +body: JSON.stringify({ + serial_number: deviceSerialNumber, + version_id: option.version_id, + price: 0.01, // <-- Tampered from 49.99! + currency: "EUR" +}) +``` + +**Why this is dangerous:** +- User can open browser console +- Change `option.price = 0.01` before payment +- Backend trusts this value = user pays 1 cent for €49.99 upgrade + +### ✅ **CORRECT APPROACH** +```javascript +// SECURE - Only send identifiers, server calculates price +body: JSON.stringify({ + serial_number: deviceSerialNumber, // Who is buying + version_id: option.version_id, // What they want + user_data: paymentData // Customer info + // NO PRICE - server calculates it! +}) +``` + +--- + +## Configuration & Requirements (USER CONFIRMED) + +1. ✅ **Mollie API Credentials**: User has Mollie info - will be added as constants in `config.php` + - `MOLLIE_API_KEY_TEST` (for sandbox) + - `MOLLIE_API_KEY_LIVE` (for production) +2. ✅ **License Duration**: `expires_at = '2099-12-31 23:59:59'` (effectively permanent until 2099) +3. ✅ **Multiple Devices**: One license per device (license linked to specific equipment_id) +4. ✅ **DEBUG Mode**: Full payment process simulation without Mollie connection AND without device connection +5. ✅ **Transaction Logging**: Use existing ecommerce transaction APIs: + - `transactions` table - main transaction record + - `transaction_items` table - line items (software upgrade details) + +--- + +## Next Steps After Plan Approval + +1. ✅ Add Mollie constants to `config.php`: + ```php + define('MOLLIE_API_KEY_TEST', 'test_xxxxx'); // User will provide + define('MOLLIE_API_KEY_LIVE', 'live_xxxxx'); // User will provide + ``` +2. ✅ Mollie PHP SDK already installed (used by existing webhook.php) +3. ✅ No database changes needed (using existing `transactions` and `transactions_items` tables) +4. Create helper functions in `assets/functions.php`: + - `generateUniqueLicenseKey()` - Generate unique license keys + - `generateSoftwareInvoice()` - Generate HTML invoice for software upgrades (based on existing generateInvoice()) +5. ✅ Payment status codes already defined in existing webhook.php +6. Implement NEW backend files: + - `/api/v2/post/payment.php` (with DEBUG mode support) + - `/api/v2/get/payment.php` + - `/webhook_mollie.php` (based on existing webhook.php structure) +8. Modify frontend JavaScript in `/assets/softwaretool.js`: + - Update `processPayment()` to call POST /v2/post/payment + - Add serial number verification in `downloadAndInstallSoftware()` +9. Modify `/softwaretool.php` to detect payment return +10. Test in DEBUG mode (full simulation without Mollie or device) +11. Test with Mollie sandbox +12. Deploy to production + diff --git a/api/v0/post/application.php b/api/v0/post/application.php index 5e61f19..6a8a550 100644 --- a/api/v0/post/application.php +++ b/api/v0/post/application.php @@ -247,10 +247,13 @@ if (!empty($post_content['sn']) && !empty($post_content['testdetails'])) { $sw_version = substr($sw_version, 0, -4); } + // Translate hardware version to standardized format + $translated_hw_version = translateDeviceHardwareVersion($hw_version); + //Update Equipment record $sql = "UPDATE equipment SET hw_version = ?, sw_version = ? $whereclause"; $stmt = $pdo->prepare($sql); - $stmt->execute([$hw_version,$sw_version]); + $stmt->execute([$translated_hw_version,$sw_version]); } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++ //Update equipment status ++++++++++++++++++++++++++ diff --git a/api/v1/get/products_software.php b/api/v1/get/products_software.php index d97bf4b..ad1d860 100644 --- a/api/v1/get/products_software.php +++ b/api/v1/get/products_software.php @@ -46,7 +46,8 @@ if(isset($get_content) && $get_content!=''){ $clause .= ' AND e.serialnumber = :'.$v[0]; } elseif ($v[0] == 'hw_version') { - //build up search + //build up search - translate hardware version for comparison + $criterias[$v[0]] = translateDeviceHardwareVersion($criterias[$v[0]]); $clause .= ' AND ps.hw_version = :'.$v[0]; } elseif ($v[0] == 'status') { @@ -152,9 +153,11 @@ if (!isset($criterias['productrowid']) && isset($criterias['sn']) && $criterias[ //check if current version is send and update the equipment record if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){ + // Translate hardware version to standardized format + $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']); $sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? '; $stmt = $pdo->prepare($sql); - $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]); + $stmt->execute([$translated_hw_version,$username,$criterias['sn']]); } //GET PRODUCTCODE, SW_VERSION_UPGRADE, HW_VERSION from equipment SN diff --git a/api/v2/get/products_software.php b/api/v2/get/products_software.php index ec50e8b..a952f40 100644 --- a/api/v2/get/products_software.php +++ b/api/v2/get/products_software.php @@ -48,7 +48,8 @@ if(isset($get_content) && $get_content!=''){ $clause .= ' AND ps.status = :'.$v[0]; } elseif ($v[0] == 'hw_version') { - //build up search + //build up search - translate hardware version for comparison + $criterias[$v[0]] = translateDeviceHardwareVersion($criterias[$v[0]]); $clause .= ' AND ps.hw_version = :'.$v[0]; } else {//create clause @@ -149,9 +150,11 @@ if (!isset($criterias['productrowid']) && isset($criterias['sn']) && $criterias[ //check if current version is send and update the equipment record if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){ + // Translate hardware version to standardized format + $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']); $sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? '; $stmt = $pdo->prepare($sql); - $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]); + $stmt->execute([$translated_hw_version,$username,$criterias['sn']]); } //GET PRODUCTCODE, SW_VERSION_UPGRADE, HW_VERSION from equipment SN diff --git a/api/v2/get/software_available.php b/api/v2/get/software_available.php index 118a914..3cb21b4 100644 --- a/api/v2/get/software_available.php +++ b/api/v2/get/software_available.php @@ -14,6 +14,7 @@ $pdo = dbConnect($dbname); //NEW ARRAY $criterias = []; $clause = ''; +$debug = []; //Check for $_GET variables and build up clause if(isset($get_content) && $get_content!=''){ @@ -27,6 +28,11 @@ if(isset($get_content) && $get_content!=''){ } } +if (debug) { + $debug['request_parameters'] = $criterias; + $debug['timestamp'] = date('Y-m-d H:i:s'); +} + // IF SN IS PROVIDED, CHECK FOR AVAILABLE UPGRADES if (isset($criterias['sn']) && $criterias['sn'] != ''){ @@ -42,9 +48,11 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ //check if current hw_version is send and update the equipment record if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){ + // Translate hardware version to standardized format + $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']); $sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? '; $stmt = $pdo->prepare($sql); - $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]); + $stmt->execute([$translated_hw_version,$username,$criterias['sn']]); } //GET EQUIPMENT AND PRODUCT DATA BASED ON SERIAL NUMBER @@ -72,8 +80,46 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ $sw_version_license = $equipment_data['sw_version_license']; $equipment_rowid = $equipment_data['equipment_rowid']; - //GET ALL DATA: active assignments, version details, and upgrade paths - //Filter on active status, hw_version compatibility, and exclude current version + if (debug) { + $debug['equipment_data'] = [ + 'product_rowid' => $product_rowid, + 'productcode' => $productcode, + 'current_sw_version_raw' => $current_sw_version, + 'hw_version' => $hw_version + ]; + } + + // Normalize software version for comparison (lowercase, trim leading zeros) + $current_sw_version = strtolower(ltrim($current_sw_version, '0')); + + // Translate incoming hw_version parameter for comparison if provided + $comparison_hw_version = $hw_version; + $hw_version_from_request = null; + if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){ + $hw_version_from_request = $criterias['hw_version']; + $comparison_hw_version = translateDeviceHardwareVersion($criterias['hw_version']); + } + + if (debug) { + $debug['normalized_data'] = [ + 'current_sw_version' => $current_sw_version, + 'hw_version_from_request' => $hw_version_from_request, + 'comparison_hw_version' => $comparison_hw_version, + 'hw_version_valid' => ($comparison_hw_version !== '') + ]; + } + + // Check if hardware version is invalid (all zeros) + if ($hw_version_from_request && $comparison_hw_version === '') { + $messages = ["software_available" => "error", "error" => "Invalid hardware version (000000) - device may not be properly initialized"]; + if (debug) { + $messages['debug'] = $debug; + } + echo json_encode($messages, JSON_UNESCAPED_UNICODE); + exit; + } + + //GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version $sql = 'SELECT psv.rowID as version_id, psv.version, @@ -82,59 +128,121 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ psv.mandatory, psv.latest, psv.hw_version, - psv.file_path, - pup.price, - pup.currency, - pup.from_version_id, - from_ver.version as from_version + psv.file_path FROM products_software_assignment psa JOIN products_software_versions psv ON psa.software_version_id = psv.rowID - LEFT JOIN products_software_upgrade_paths pup ON pup.to_version_id = psv.rowID AND pup.is_active = 1 - LEFT JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID WHERE psa.product_id = ? AND psa.status = 1 - AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "") - AND psv.version != ?'; + AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")'; $stmt = $pdo->prepare($sql); - $stmt->execute([$product_rowid, $hw_version, $current_sw_version ?? '']); + $stmt->execute([$product_rowid, $comparison_hw_version]); $versions = $stmt->fetchAll(PDO::FETCH_ASSOC); + if (debug) { + $debug['active_assignments'] = [ + 'count' => count($versions), + 'versions' => array_map(function($v) { + return [ + 'version_id' => $v['version_id'], + 'version' => $v['version'], + 'name' => $v['name'], + 'hw_version' => $v['hw_version'], + 'latest' => $v['latest'] + ]; + }, $versions) + ]; + } + if (empty($versions)) { // No versions available $software_available = "no"; + if (debug) { + $debug['decision'] = 'No active software assignments found'; + } } else { $has_priced_options = false; $has_latest_version_different = false; - + + if (debug) { + $debug['version_checks'] = []; + } + foreach ($versions as $version) { + //Normalize version for comparison (lowercase, trim leading zeros) + $normalized_version = strtolower(ltrim($version['version'], '0')); + + //Skip if this is the current version + if ($current_sw_version && $normalized_version == $current_sw_version) { + continue; + } + //Check if this version should be shown (same logic as software_update) $show_version = false; + $final_price = '0.00'; + $decision_reason = ''; + + if (debug) { + $version_check = [ + 'version' => $version['version'], + 'name' => $version['name'], + 'normalized' => $normalized_version, + 'is_current' => ($current_sw_version && $normalized_version == $current_sw_version) + ]; + } + if (!$current_sw_version || $current_sw_version == '') { //No current version - show all $show_version = true; - } elseif ($version['from_version'] == $current_sw_version) { - //Upgrade path exists from current version - $show_version = true; + $decision_reason = 'No current version - showing all'; } else { - //Check if any upgrade paths exist for this version + //Check if this version is part of ANY upgrade path system (either FROM or TO) $sql = 'SELECT COUNT(*) as path_count FROM products_software_upgrade_paths - WHERE to_version_id = ? AND is_active = 1'; + WHERE (to_version_id = ? OR from_version_id = ?) AND is_active = 1'; $stmt = $pdo->prepare($sql); - $stmt->execute([$version['version_id']]); + $stmt->execute([$version['version_id'], $version['version_id']]); $path_check = $stmt->fetch(PDO::FETCH_ASSOC); - if ($path_check['path_count'] == 0) { - //No paths exist at all - show as free upgrade - $show_version = true; + if (debug) { + $version_check['path_count'] = $path_check['path_count']; } + + if ($path_check['path_count'] == 0) { + //Not part of any upgrade path system - show as free upgrade + $show_version = true; + $decision_reason = 'No upgrade paths defined - showing as free'; + } else { + //Part of an upgrade path system + //Only show if there's an explicit path FROM current version TO this version + $sql = 'SELECT pup.price, pup.currency + FROM products_software_upgrade_paths pup + JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID + WHERE pup.to_version_id = ? + AND LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ? + AND pup.is_active = 1'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$version['version_id'], $current_sw_version]); + $upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($upgrade_path) { + //Valid upgrade path found FROM current version + $show_version = true; + $final_price = $upgrade_path['price'] ?? '0.00'; + $decision_reason = 'Found upgrade path from current with price: ' . $final_price; + } else { + $decision_reason = 'Has upgrade paths but none from current version'; + } + } + } + + if (debug) { + $version_check['show_version'] = $show_version; + $version_check['reason'] = $decision_reason; } if ($show_version) { //Check if there's a valid license for this upgrade - $final_price = $version['price'] ?? '0.00'; - if ($final_price > 0 && $sw_version_license) { //Check if the license is valid $sql = 'SELECT status, start_at, expires_at @@ -162,26 +270,52 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ } // Check if there's a "latest" flagged version that's different from current - if ($version['latest'] == 1 && $version['version'] != $current_sw_version) { + if ($version['latest'] == 1 && $normalized_version != $current_sw_version) { $has_latest_version_different = true; } + + if (debug) { + $version_check['final_price'] = $final_price; + $version_check['has_priced_option'] = ($final_price > 0); + $version_check['is_latest_different'] = ($version['latest'] == 1 && $normalized_version != $current_sw_version); + } + } + + if (debug) { + $debug['version_checks'][] = $version_check; } } // Apply the logic: // 1. If there are priced options -> "yes" - // 2. If no priced options but current version != latest flagged version -> "yes" + // 2. If no priced options but current version != latest flagged version -> "yes" // 3. Default -> "no" if ($has_priced_options) { $software_available = "yes"; + $availability_reason = "Has priced upgrade options available"; } elseif ($has_latest_version_different) { $software_available = "yes"; + $availability_reason = "Has free latest version available"; } else { $software_available = "no"; + $availability_reason = "No upgrades available or already on latest"; + } + + if (debug) { + $debug['final_decision'] = [ + 'has_priced_options' => $has_priced_options, + 'has_latest_version_different' => $has_latest_version_different, + 'software_available' => $software_available, + 'reason' => $availability_reason + ]; } } $messages = ["software_available" => $software_available]; + + if (debug) { + debuglog(json_encode($debug)); + } } } else { $messages = ["error" => "No serialnumber found"]; diff --git a/api/v2/get/software_download.php b/api/v2/get/software_download.php index 4d6d736..5260d91 100644 --- a/api/v2/get/software_download.php +++ b/api/v2/get/software_download.php @@ -9,8 +9,6 @@ defined($security_key) or exit; //Connect to DB $pdo = dbConnect($dbname); -var_dump($_GET); - // STEP 1: Validate token parameter exists if (!isset($_GET['token']) || $_GET['token'] == '') { http_response_code(400); @@ -135,8 +133,10 @@ if ($assignment['assigned'] == 0) { } // STEP 6: Hardware version compatibility -if ($version['hw_version'] && $version['hw_version'] != '' && $equipment['hw_version']) { - if ($version['hw_version'] != $equipment['hw_version']) { +// Only check if version has hw_version requirement (not NULL or empty) +// Match logic from software_update.php line 103 +if ($version['hw_version'] && $version['hw_version'] != '') { + if ($equipment['hw_version'] && $version['hw_version'] != $equipment['hw_version']) { http_response_code(403); log_download([ 'user_id' => $user_data['id'], diff --git a/api/v2/get/software_update.php b/api/v2/get/software_update.php index 7abf15e..3a71457 100644 --- a/api/v2/get/software_update.php +++ b/api/v2/get/software_update.php @@ -13,6 +13,7 @@ $pdo = dbConnect($dbname); //NEW ARRAY $criterias = []; $clause = ''; +$debug = []; //Check for $_GET variables and build up clause if(isset($get_content) && $get_content!=''){ @@ -26,6 +27,11 @@ if(isset($get_content) && $get_content!=''){ } } +if (debug) { + $debug['request_parameters'] = $criterias; + $debug['timestamp'] = date('Y-m-d H:i:s'); +} + // IF SN IS PROVIDED, HANDLE UPGRADE OPTIONS if (isset($criterias['sn']) && $criterias['sn'] != ''){ @@ -41,9 +47,11 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ //check if current hw_version is send and update the equipment record if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){ + // Translate hardware version to standardized format + $translated_hw_version = translateDeviceHardwareVersion($criterias['hw_version']); $sql = 'UPDATE equipment SET hw_version = ?, updatedby = ? WHERE serialnumber = ? '; $stmt = $pdo->prepare($sql); - $stmt->execute([$criterias['hw_version'],$username,$criterias['sn']]); + $stmt->execute([$translated_hw_version,$username,$criterias['sn']]); } //GET EQUIPMENT AND PRODUCT DATA BASED ON SERIAL NUMBER @@ -71,8 +79,47 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ $sw_version_license = $equipment_data['sw_version_license']; $equipment_rowid = $equipment_data['equipment_rowid']; - //GET ALL DATA: active assignments, version details, and upgrade paths - //Filter on active status and hw_version compatibility + if (debug) { + $debug['equipment_data'] = [ + 'product_rowid' => $product_rowid, + 'productcode' => $productcode, + 'current_sw_version_raw' => $current_sw_version, + 'hw_version' => $hw_version, + 'sw_version_license' => $sw_version_license + ]; + } + + // Normalize software version for comparison (lowercase, trim leading zeros) + $current_sw_version = strtolower(ltrim($current_sw_version, '0')); + + // Translate incoming hw_version parameter for comparison if provided + $comparison_hw_version = $hw_version; + $hw_version_from_request = null; + if(isset($criterias['hw_version']) && $criterias['hw_version'] !=''){ + $hw_version_from_request = $criterias['hw_version']; + $comparison_hw_version = translateDeviceHardwareVersion($criterias['hw_version']); + } + + if (debug) { + $debug['normalized_data'] = [ + 'current_sw_version' => $current_sw_version, + 'hw_version_from_request' => $hw_version_from_request, + 'comparison_hw_version' => $comparison_hw_version, + 'hw_version_valid' => ($comparison_hw_version !== '') + ]; + } + + // Check if hardware version is invalid (all zeros) + if ($hw_version_from_request && $comparison_hw_version === '') { + $messages = ["error" => "Invalid hardware version (000000) - device may not be properly initialized"]; + if (debug) { + $messages['debug'] = $debug; + } + echo json_encode($messages, JSON_UNESCAPED_UNICODE); + exit; + } + + //GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version $sql = 'SELECT psv.rowID as version_id, psv.version, @@ -81,60 +128,157 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ psv.mandatory, psv.latest, psv.hw_version, - psv.file_path, - pup.price, - pup.currency, - pup.from_version_id, - from_ver.version as from_version + psv.file_path FROM products_software_assignment psa JOIN products_software_versions psv ON psa.software_version_id = psv.rowID - LEFT JOIN products_software_upgrade_paths pup ON pup.to_version_id = psv.rowID AND pup.is_active = 1 - LEFT JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID WHERE psa.product_id = ? AND psa.status = 1 - AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "") - AND (? IS NULL OR ? = "" OR psv.version != ?)'; + AND (psv.hw_version = ? OR psv.hw_version IS NULL OR psv.hw_version = "")'; $stmt = $pdo->prepare($sql); - $stmt->execute([$product_rowid, $hw_version, $current_sw_version, $current_sw_version, $current_sw_version]); + $stmt->execute([$product_rowid, $comparison_hw_version]); $versions = $stmt->fetchAll(PDO::FETCH_ASSOC); + if (debug) { + $debug['active_assignments'] = [ + 'count' => count($versions), + 'versions' => array_map(function($v) { + return [ + 'version_id' => $v['version_id'], + 'version' => $v['version'], + 'name' => $v['name'], + 'hw_version' => $v['hw_version'], + 'latest' => $v['latest'] + ]; + }, $versions) + ]; + } + if (empty($versions)) { $messages = ["error" => "No active software assignments found for product"]; + if (debug) { + $messages['debug'] = $debug; + } } else { + // First check if current version has paid upgrade paths FROM it + $has_paid_upgrade_from_current = false; + if ($current_sw_version) { + $sql = 'SELECT COUNT(*) as paid_count + FROM products_software_upgrade_paths pup + JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID + WHERE LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ? + AND pup.price > 0 + AND pup.is_active = 1'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$current_sw_version]); + $paid_check = $stmt->fetch(PDO::FETCH_ASSOC); + $has_paid_upgrade_from_current = ($paid_check['paid_count'] > 0); + } + + if (debug) { + $debug['has_paid_upgrade_from_current'] = $has_paid_upgrade_from_current; + $debug['version_decisions'] = []; + } + foreach ($versions as $version) { - //Check if this version should be shown: - //1. If there's a matching upgrade path from current version, show it - //2. If no current version exists, show all - //3. If there's no upgrade path but also no paths exist for this version at all, show it (free upgrade) + //Normalize version for comparison (lowercase, trim leading zeros) + $normalized_version = strtolower(ltrim($version['version'], '0')); + $is_current_version = ($current_sw_version && $normalized_version == $current_sw_version); + + //All versions with matching HW are potential upgrades $show_version = false; - if (!$current_sw_version || $current_sw_version == '') { - //No current version - show all - $show_version = true; - } elseif ($version['from_version'] == $current_sw_version) { - //Upgrade path exists from current version - $show_version = true; - } else { - //Check if any upgrade paths exist for this version - $sql = 'SELECT COUNT(*) as path_count - FROM products_software_upgrade_paths - WHERE to_version_id = ? AND is_active = 1'; - $stmt = $pdo->prepare($sql); - $stmt->execute([$version['version_id']]); - $path_check = $stmt->fetch(PDO::FETCH_ASSOC); + $final_price = '0.00'; + $final_currency = ''; + $is_current = false; + $decision_reason = ''; - if ($path_check['path_count'] == 0) { - //No paths exist at all - show as free upgrade + if (debug) { + $version_debug = [ + 'version' => $version['version'], + 'name' => $version['name'], + 'normalized_version' => $normalized_version, + 'is_current_version' => $is_current_version, + 'latest' => $version['latest'] + ]; + } + + if (!$current_sw_version || $current_sw_version == '') { + //No current version - show all as free upgrades + if (!$is_current_version) { $show_version = true; + $decision_reason = 'No current version stored - showing as free upgrade'; + } else { + $decision_reason = 'Skipped - is current version but no upgrades scenario'; } + } else { + //Check if this is the current version and should be shown as disabled + if ($is_current_version && $has_paid_upgrade_from_current && $version['latest'] == 1) { + //Show current version as disabled only if it's the latest AND there's a paid upgrade available + $show_version = true; + $is_current = true; + $final_price = '0.00'; + $final_currency = ''; + $decision_reason = 'Showing as CURRENT - is latest version with paid upgrade available'; + } else if ($is_current_version && !($has_paid_upgrade_from_current && $version['latest'] == 1)) { + $decision_reason = 'Skipped - is current version but not (latest + has_paid_upgrade)'; + } else if (!$is_current_version) { + //Check if this version is part of ANY upgrade path system (either FROM or TO) + $sql = 'SELECT COUNT(*) as path_count + FROM products_software_upgrade_paths + WHERE (to_version_id = ? OR from_version_id = ?) AND is_active = 1'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$version['version_id'], $version['version_id']]); + $path_check = $stmt->fetch(PDO::FETCH_ASSOC); + + if (debug) { + $version_debug['upgrade_path_count'] = $path_check['path_count']; + } + + if ($path_check['path_count'] == 0) { + //Not part of any upgrade path system - show as free upgrade + $show_version = true; + $decision_reason = 'Showing as FREE - no upgrade paths defined for this version'; + } else { + //Part of an upgrade path system + //Only show if there's an explicit path FROM current version TO this version + $sql = 'SELECT pup.price, pup.currency + FROM products_software_upgrade_paths pup + JOIN products_software_versions from_ver ON pup.from_version_id = from_ver.rowID + WHERE pup.to_version_id = ? + AND LOWER(TRIM(LEADING "0" FROM from_ver.version)) = ? + AND pup.is_active = 1'; + $stmt = $pdo->prepare($sql); + $stmt->execute([$version['version_id'], $current_sw_version]); + $upgrade_path = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($upgrade_path) { + //Valid upgrade path found FROM current version + $show_version = true; + $final_price = $upgrade_path['price'] ?? '0.00'; + $final_currency = $upgrade_path['currency'] ?? ''; + $decision_reason = 'Showing - found upgrade path FROM current (' . $current_sw_version . ') with price: ' . $final_price . ' ' . $final_currency; + } else { + $decision_reason = 'Skipped - has upgrade paths but none FROM current version (' . $current_sw_version . ')'; + } + //If no path from current version exists, don't show (show_version stays false) + } + } + } + + if (debug) { + $version_debug['decision'] = [ + 'show_version' => $show_version, + 'is_current' => $is_current, + 'final_price' => $final_price, + 'final_currency' => $final_currency, + 'reason' => $decision_reason + ]; } if ($show_version) { //Check if there's a valid license for this upgrade - $final_price = $version['price'] ?? '0.00'; - $final_currency = $version['currency'] ?? ''; - + $license_applied = false; if ($final_price > 0 && $sw_version_license) { //Check if the license is valid $sql = 'SELECT status, start_at, expires_at @@ -151,7 +295,17 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ //Check if license is within valid date range if ((!$start_at || $start_at <= $now) && (!$expires_at || $expires_at >= $now)) { + $original_price = $final_price; $final_price = '0.00'; + $license_applied = true; + + if (debug) { + $version_debug['license_applied'] = [ + 'license_key' => $sw_version_license, + 'original_price' => $original_price, + 'new_price' => $final_price + ]; + } } } } @@ -169,9 +323,14 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ "source" => '', "source_type" => '', "price" => $final_price, - "currency" => $final_currency + "currency" => $final_currency, + "is_current" => $is_current ]; } + + if (debug) { + $debug['version_decisions'][] = $version_debug; + } } //GENERATE DOWNLOAD TOKENS FOR EACH OPTION @@ -180,13 +339,38 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){ $download_token = create_download_url_token($criterias['sn'], $option['version_id']); // Create secure download URL - $download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download/token='.$download_token; + $download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download?token='.$download_token; // Set source as download URL $option['source'] = $download_url; $option['source_type'] = 'token_url'; } + + if (debug) { + $debug['final_output'] = [ + 'total_versions_shown' => count($output), + 'versions' => array_map(function($o) { + return [ + 'name' => $o['name'], + 'version' => $o['version'], + 'price' => $o['price'], + 'is_current' => $o['is_current'] + ]; + }, $output) + ]; + } + $messages = $output; + + if (debug && !empty($output)) { + // Add debug as separate field in response + foreach ($messages as &$msg) { + $msg['_debug'] = $debug; + break; // Only add to first item + } + } elseif (debug && empty($output)) { + $messages = ['message' => 'No upgrades available', 'debug' => $debug]; + } } } } diff --git a/api/v2/post/history.php b/api/v2/post/history.php index 2ad217f..75eead9 100644 --- a/api/v2/post/history.php +++ b/api/v2/post/history.php @@ -233,10 +233,13 @@ if (isset($post_content['sn']) && isset($post_content['payload'])){ $sw_version = substr($sw_version, 0, -4); } + // Translate hardware version to standardized format + $translated_hw_version = translateDeviceHardwareVersion($hw_version); + //Update Equipment record $sql = "UPDATE equipment SET hw_version = ?, sw_version = ? $whereclause"; $stmt = $pdo->prepare($sql); - $stmt->execute([$hw_version,$sw_version]); + $stmt->execute([$translated_hw_version,$sw_version]); } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++ //Update equipment status ++++++++++++++++++++++++++ diff --git a/api/v2/post/products_software_upgrade_paths.php b/api/v2/post/products_software_upgrade_paths.php index d3849bb..6b6f9a4 100644 --- a/api/v2/post/products_software_upgrade_paths.php +++ b/api/v2/post/products_software_upgrade_paths.php @@ -62,16 +62,25 @@ $clause = substr($clause, 2); //Clean clause - remove first comma $clause_insert = substr($clause_insert, 2); //Clean clause - remove first comma $input_insert = substr($input_insert, 1); //Clean clause - remove first comma +//VALIDATE: Prevent FROM and TO being the same version +if (($command == 'insert' || $command == 'update') && + isset($criterias['from_version_id']) && isset($criterias['to_version_id']) && + $criterias['from_version_id'] == $criterias['to_version_id']) { + http_response_code(400); + echo json_encode(["error" => "FROM version cannot be the same as TO version in upgrade path"]); + exit; +} + //QUERY AND VERIFY ALLOWED if ($command == 'update' && isAllowed('products_software_upgrade_paths',$profile,$permission,'U') === 1){ - + $sql = 'UPDATE products_software_upgrade_paths SET '.$clause.' WHERE rowID = ? '; $execute_input[] = $id; $stmt = $pdo->prepare($sql); $stmt->execute($execute_input); -} +} elseif ($command == 'insert' && isAllowed('products_software_upgrade_paths',$profile,$permission,'C') === 1){ - + //INSERT NEW ITEM $sql = 'INSERT INTO products_software_upgrade_paths ('.$clause_insert.') VALUES ('.$input_insert.')'; $stmt = $pdo->prepare($sql); diff --git a/api/v2/post/products_software_versions.php b/api/v2/post/products_software_versions.php index 52f4191..60417ab 100644 --- a/api/v2/post/products_software_versions.php +++ b/api/v2/post/products_software_versions.php @@ -41,8 +41,11 @@ else { //do nothing } +//translate HW_VERSION to correct string +if (isset($post_content['hw_version']) && $post_content['hw_version'] !=''){$post_content['hw_version'] =translateDeviceHardwareVersion($post_content['hw_version']); } + //CREATE NEW ARRAY AND MAP TO CLAUSE -if(isset($post_content) && $post_content!=''){ +if(isset($post_content) && $post_content!=''){ foreach ($post_content as $key => $var){ if ($key == 'submit' || $key == 'rowID'){ //do nothing diff --git a/assets/functions.php b/assets/functions.php index 608a441..69c7486 100644 --- a/assets/functions.php +++ b/assets/functions.php @@ -5191,4 +5191,101 @@ function updateSoftwareVersionStatus($pdo, $serialnumber = null) { error_log('Database error in updateSoftwareVersionStatus: ' . $e->getMessage()); return false; } +} + +// +++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// Hardware Version Translation Functions +// +++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +/** + * Translates hardware version to standardized format + * Examples: + * - r80, R80, 80 -> r08 + * - r70, R70, 70 -> r07 + * - r60, R60, 60 -> r06 + * etc. + * + * @param string $hw_version - Input hardware version + * @return string - Standardized hardware version + */ +function translateHardwareVersion($hw_version) { + if (empty($hw_version) || $hw_version == '') { + return $hw_version; + } + + // Remove any whitespace and convert to lowercase for processing + $hw_clean = strtolower(trim($hw_version)); + + // Treat all-zeros as invalid/empty hardware version + if (preg_match('/^0+$/', $hw_clean)) { + return ''; + } + + // Define translation mapping + $translation_map = [ + // r80/R80/80 variants -> r08 + 'r80' => 'r08', + '80' => 'r08', + + // r70/R70/70 variants -> r07 + 'r70' => 'r07', + '70' => 'r07', + + // r60/R60/60 variants -> r06 + 'r60' => 'r06', + '60' => 'r06', + + // Already correct format, just ensure lowercase + 'r08' => 'r08', + '08' => 'r08', + 'r07' => 'r07', + '07' => 'r07', + 'r06' => 'r06', + '06' => 'r06', + ]; + + // Check if we have a direct mapping + if (isset($translation_map[$hw_clean])) { + return $translation_map[$hw_clean]; + } + + // Handle pattern matching for other potential formats + // Extract numeric value from various formats (00000080, r90, 90, etc.) + if (preg_match('/^r?0*(\d{1,2})$/', $hw_clean, $matches)) { + $number = intval($matches[1]); + if ($number >= 10 && $number <= 99) { + // Convert to zero-padded format: 80 -> 08, 70 -> 07, etc. + // Take the tens digit and format as 0X: 80->08, 70->07, 60->06 + $tensDigit = intval($number / 10); + $padded = '0' . $tensDigit; + return 'r' . $padded; + } + } + + // If no translation found, return original input unchanged + return $hw_version; +} + +/** + * Translates hardware version received from device/API to standardized DB format + * This should be called before storing hw_version in the database + * + * @param string $device_hw_version - Hardware version from device + * @return string - Standardized hardware version for database storage + */ +function translateDeviceHardwareVersion($device_hw_version) { + return translateHardwareVersion($device_hw_version); +} + +/** + * Translates hardware version from database to match device format if needed + * This can be used for display or API responses + * + * @param string $db_hw_version - Hardware version from database + * @return string - Hardware version (currently returns same as input) + */ +function translateDbHardwareVersion($db_hw_version) { + // For now, we keep the standardized format from DB + // This function exists for future reverse translation if needed + return $db_hw_version; } \ No newline at end of file diff --git a/assets/softwaretool.js b/assets/softwaretool.js new file mode 100644 index 0000000..cb371c8 --- /dev/null +++ b/assets/softwaretool.js @@ -0,0 +1,1099 @@ +const serialResultsDiv = document.getElementById("serialResults"); +const readBar = document.getElementById("readBar"); + +// Buffer for accumulating received data before logging +let receivedDataBuffer = ''; + +// Software tool specific variables +let deviceSerialNumber = ""; +let deviceVersion = ""; +let deviceHwVersion = ""; +let selectedSoftwareUrl = ""; + +// Serial port variables (port, writer, textEncoder, writableStreamClosed declared in PHP) +let reader; +let readableStreamClosed; +let keepReading = true; + +// Function to log communication to API (reused from scripts.js) +async function logCommunication(data, direction) { + // Only log if debug mode is enabled + if (typeof DEBUG === 'undefined' || !DEBUG) { + return; + } + + try { + const serviceToken = document.getElementById("servicetoken")?.innerHTML || ''; + + let serialNumber = ''; + if (deviceSerialNumber) { + serialNumber = deviceSerialNumber; + } + + const logData = { + data: data, + direction: direction, + timestamp: new Date().toISOString(), + serial_number: serialNumber, + maintenance_run: 0 + }; + + const url = link + '/v2/com_log/log'; + const bearer = 'Bearer ' + serviceToken; + + const response = await fetch(url, { + method: 'POST', + withCredentials: true, + credentials: 'include', + headers: { + 'Authorization': bearer, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(logData) + }); + + if (!response.ok) { + console.warn('Failed to log communication:', response.status); + } + } catch (error) { + console.warn('Error logging communication:', error); + } +} + +// Progress bar function (reused from scripts.js) +function progressBar(percentage, message, color){ + readBar.style.background = color; + readBar.style.width = percentage +"%"; + readBar.innerHTML = message; +} + +// Connect device for software tool +async function connectDeviceForSoftware() { + //clear input + readBar.innerHTML = ''; + serialResultsDiv.innerHTML = ''; + document.getElementById("softwareCheckStatus").style.display = "none"; + document.getElementById("softwareOptions").style.display = "none"; + document.getElementById("noUpdatesMessage").style.display = "none"; + document.getElementById("uploadSection").style.display = "none"; + + // Reset data + receivedDataBuffer = ''; + deviceSerialNumber = ""; + deviceVersion = ""; + deviceHwVersion = ""; + + //set progress bar + progressBar("1", "", ""); + + // Check if DEBUG mode is enabled - use mock device data + if (typeof DEBUG !== 'undefined' && DEBUG) { + // TEST MODE: Use mock device data + deviceSerialNumber = "22110095"; + deviceVersion = "03e615af"; + deviceHwVersion = "0000080"; + + document.getElementById("Device_output").style.display = "block"; + serialResultsDiv.innerHTML = `DEBUG MODE - Simulated Device Data:
SN=${deviceSerialNumber}
FW=${deviceVersion}
HW=${deviceHwVersion}`; + progressBar("60", "DEBUG MODE - Device data read - SN: " + deviceSerialNumber, "#ff9800"); + + await logCommunication(`DEBUG MODE: Simulated device - SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake'); + + // Proceed to check software availability + checkSoftwareAvailability(); + return; + } + + // Check if port is already open - if so, refresh the page to start clean + if (port) { + await logCommunication('Port already in use - refreshing page for clean reconnect', 'info'); + location.reload(); + return; + } + + // Reset flags for new connection + keepReading = true; + + try { + // Prompt user to select any serial port. + const filters = [{ usbVendorId: 1027, usbProductId: 24597 }]; + port = await navigator.serial.requestPort({ filters }); + + // Log selected port details + const portInfo = port.getInfo(); + const portDetails = { + processStep: 'Software', + usbVendorId: portInfo.usbVendorId, + usbProductId: portInfo.usbProductId, + readable: !!port.readable, + writable: !!port.writable, + opened: port.readable !== null && port.writable !== null + }; + await logCommunication(`Selected USB device - ${JSON.stringify(portDetails)}`, 'connected'); + + await port.open({ + baudRate: 56700, + dataBits: 8, + stopBits: 1, + parity: 'none', + flowControl: 'none' + }); + + progressBar("10", "Connecting", "#04AA6D"); + + // Log successful connection + await logCommunication('Port opened successfully', 'connected'); + + textEncoder = new TextEncoderStream(); + writableStreamClosed = textEncoder.readable.pipeTo(port.writable); + writer = textEncoder.writable.getWriter(); + + progressBar("20", "Connected, reading device...", "#04AA6D"); + + // Read device output + await readDeviceOutput(); + + // Close the port immediately after reading + await closePortAfterRead(); + + // Check for software updates (port is now closed) + checkSoftwareAvailability(); + + } catch (error) { + await logCommunication(`Connection error: ${error.message}`, 'error'); + progressBar("0", "Error: " + error.message, "#ff6666"); + } +} + +async function readDeviceOutput() { + const textDecoder = new TextDecoderStream(); + readableStreamClosed = port.readable.pipeTo(textDecoder.writable); + reader = textDecoder.readable.getReader(); + + document.getElementById("Device_output").style.display = "block"; + + let dataCompleteMarkerFound = false; + const startTime = Date.now(); + const timeout = 10000; // 10 second timeout + + try { + progressBar("40", "Reading device...", "#04AA6D"); + + while (keepReading) { + const { value, done } = await reader.read(); + if (done || !keepReading) { + break; + } + + receivedDataBuffer += value; + serialResultsDiv.innerHTML = receivedDataBuffer; + + // Check if we have received PLUGTY which comes after all device data + // Or wait for at least FWDATE which comes near the end + if (receivedDataBuffer.indexOf("PLUGTY") > 0 || receivedDataBuffer.indexOf("FWDATE=") > 0) { + dataCompleteMarkerFound = true; + } + + // Only parse and exit if we found the completion marker + if (dataCompleteMarkerFound) { + // Parse device info from complete output + parseDeviceInfo(receivedDataBuffer); + + // Check if we have all required data + if (deviceSerialNumber && deviceVersion && deviceHwVersion) { + progressBar("60", "Device data read - SN: " + deviceSerialNumber, "#04AA6D"); + await logCommunication(`Device identification data received: SN=${deviceSerialNumber}, Version=${deviceVersion}, HW=${deviceHwVersion}`, 'handshake'); + + // Exit cleanly + break; + } + } + + // Timeout check + if (Date.now() - startTime > timeout) { + await logCommunication('Device read timeout - proceeding with partial data', 'error'); + parseDeviceInfo(receivedDataBuffer); + break; + } + } + + progressBar("65", "Device reading completed", "#04AA6D"); + + } catch (error) { + if (keepReading) { + await logCommunication(`Read error: ${error.message}`, 'error'); + progressBar("0", "Error reading device: " + error.message, "#ff6666"); + } + } finally { + // Clean up reader and cancel the stream + try { + if (reader) { + await reader.cancel(); + reader.releaseLock(); + } + } catch (e) { + console.log('Reader cleanup error:', e); + } + } +} + +function parseDeviceInfo(data) { + // Extract SN, FW (firmware/software version), and HW from device output + // Device format: SN=12345678;FW=12345678;HW=12345678;STATE=... + // Match exact logic from scripts.js and readdevice.js + + // Use exact same approach as scripts.js line 153 and readdevice.js line 649 + const x = Array.from(new Set(data.split(";"))).toString(); + + // Parse SN= (8 characters, exact logic from scripts.js line 185-189) + if (x.indexOf("SN=") > 0 && !deviceSerialNumber) { + const a = x.indexOf("SN="); + const b = a + 3; + const c = b + 8; + deviceSerialNumber = x.substring(b, c); + console.log("Found SN:", deviceSerialNumber); + } + + // Parse FW= (8 characters, exact logic from readdevice.js line 672-676) + if (x.indexOf("FW=") > 0 && !deviceVersion) { + const a = x.indexOf("FW="); + const b = a + 3; + const c = b + 8; + deviceVersion = x.substring(b, c); + console.log("Found FW/Version:", deviceVersion); + } + + // Parse HW= (8 characters, exact logic from readdevice.js line 665-670) + if (x.indexOf("HW=") > 0 && !deviceHwVersion) { + const a = x.indexOf("HW="); + const b = a + 3; + const c = b + 8; + deviceHwVersion = x.substring(b, c); + console.log("Found HW Version:", deviceHwVersion); + } +} + +async function closePortAfterRead() { + if (port) { + try { + await logCommunication('Closing port after reading device data', 'info'); + + // Reader is already cancelled and released by readDeviceOutput finally block + // Now abort the writer + if (writer) { + try { + await writer.abort(); + } catch (e) { + console.log('Writer abort error:', e); + } + } + + // Give time for streams to cleanup + await new Promise(resolve => setTimeout(resolve, 300)); + + // Close the port + await port.close(); + await logCommunication('Port closed successfully', 'info'); + + // Reset for next connection + reader = null; + writer = null; + readableStreamClosed = null; + writableStreamClosed = null; + port = null; + } catch (error) { + console.error('Error closing port after read:', error); + await logCommunication(`Error closing port: ${error.message}`, 'error'); + + // Force reset even on error + reader = null; + writer = null; + readableStreamClosed = null; + writableStreamClosed = null; + port = null; + } + } +} + +async function closePort() { + if (port) { + try { + keepReading = false; + + // Wait for read operations to complete + await new Promise(resolve => setTimeout(resolve, 300)); + + // Release reader if still locked + if (reader) { + try { + reader.releaseLock(); + } catch (e) { + console.log('Reader release error:', e); + } + } + + // Wait a bit more + await new Promise(resolve => setTimeout(resolve, 200)); + + // Now close the port + try { + await port.close(); + await logCommunication('Port closed', 'info'); + } catch (e) { + console.log('Port close error (may already be closed):', e); + } + + // Reset for next connection + reader = null; + port = null; + } catch (error) { + console.error('Error closing port:', error); + } + } +} + +async function checkSoftwareAvailability() { + if (!deviceSerialNumber) { + progressBar("0", "Error: Serial number not found", "#ff6666"); + await logCommunication('Serial number not found in device output', 'error'); + return; + } + + document.getElementById("softwareCheckStatus").style.display = "block"; + progressBar("70", "Checking for software updates...", "#04AA6D"); + + try { + // Call software_available API + const availableUrl = link + "/v2/software_available/sn=" + deviceSerialNumber + + (deviceVersion ? "&version=" + deviceVersion : "") + + (deviceHwVersion ? "&hw_version=" + deviceHwVersion : ""); + + console.log("Calling API:", availableUrl); + await logCommunication(`Checking software availability for SN: ${deviceSerialNumber}`, 'sent'); + + const availableResponse = await fetch(availableUrl, { + method: "GET", + headers: { + "Authorization": "Bearer " + document.getElementById("servicetoken").textContent, + "Content-Type": "application/json" + } + }); + + const availableData = await availableResponse.json(); + console.log("Available response:", availableData); + + await logCommunication(`Software availability response: ${JSON.stringify(availableData)}`, 'received'); + + if (availableData.software_available === "error" || availableData.error) { + // Error checking for updates (e.g., invalid hardware version) + document.getElementById("softwareCheckStatus").style.display = "none"; + progressBar("0", "Error: " + (availableData.error || "Unable to check for updates"), "#ff6666"); + alert("Error checking for updates: " + (availableData.error || "Unknown error")); + } else if (availableData.software_available === "yes") { + // Software updates available, fetch options + progressBar("80", "Software updates found, loading options...", "#04AA6D"); + await fetchSoftwareOptions(); + } else { + // No updates available + document.getElementById("softwareCheckStatus").style.display = "none"; + document.getElementById("noUpdatesMessage").style.display = "block"; + progressBar("100", "No software updates available", "#04AA6D"); + } + } catch (error) { + await logCommunication(`Software check error: ${error.message}`, 'error'); + progressBar("0", "Error checking software: " + error.message, "#ff6666"); + await closePort(); + } +} + +async function fetchSoftwareOptions() { + try { + // Call software_update API to get options + const updateUrl = link + "/v2/software_update/sn=" + deviceSerialNumber + + (deviceVersion ? "&version=" + deviceVersion : "") + + (deviceHwVersion ? "&hw_version=" + deviceHwVersion : ""); + + console.log("Fetching options from:", updateUrl); + await logCommunication(`Fetching software options for SN: ${deviceSerialNumber}`, 'sent'); + + const updateResponse = await fetch(updateUrl, { + method: "GET", + headers: { + "Authorization": "Bearer " + document.getElementById("servicetoken").textContent, + "Content-Type": "application/json" + } + }); + + const options = await updateResponse.json(); + console.log("Software options:", options); + + await logCommunication(`Software options response: ${JSON.stringify(options)}`, 'received'); + + if (options.error) { + document.getElementById("softwareCheckStatus").style.display = "none"; + document.getElementById("noUpdatesMessage").style.display = "block"; + progressBar("100", "No software updates available", "#04AA6D"); + await closePort(); + return; + } + + // Display options in table + displaySoftwareOptions(options); + document.getElementById("softwareCheckStatus").style.display = "none"; + document.getElementById("softwareOptions").style.display = "block"; + progressBar("100", "Software options loaded", "#04AA6D"); + + } catch (error) { + await logCommunication(`Software options error: ${error.message}`, 'error'); + progressBar("0", "Error loading options: " + error.message, "#ff6666"); + await closePort(); + } +} + +function displaySoftwareOptions(options) { + const grid = document.getElementById("softwareOptionsGrid"); + grid.innerHTML = ""; + + options.forEach((option, index) => { + const price = parseFloat(option.price); + const isFree = price === 0; + const isCurrent = option.is_current === true || option.is_current === 1; + + // Create card + const card = document.createElement("div"); + card.style.cssText = ` + background: ${isCurrent ? '#f5f5f5' : 'white'}; + border: 2px solid ${isCurrent ? '#bbb' : (isFree ? '#e0e0e0' : '#e0e0e0')}; + border-radius: 4px; + padding: 15px; + transition: 0.3s; + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + transform: translateY(0px); + box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px; + opacity: ${isCurrent ? '0.6' : '1'}; + pointer-events: ${isCurrent ? 'none' : 'auto'}; + `; + + if (!isCurrent) { + card.onmouseenter = () => { + card.style.transform = 'translateY(-5px)'; + card.style.boxShadow = '0 8px 16px rgba(0,0,0,0.15)'; + }; + card.onmouseleave = () => { + card.style.transform = 'translateY(0)'; + card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)'; + }; + } + + // Badge for current/free/paid + const badge = document.createElement("div"); + badge.style.cssText = ` + position: absolute; + top: 15px; + right: 15px; + background: ${isCurrent ? '#6c757d' : '#04AA6D'}; + color: white; + padding: 5px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: bold; + display:none; + `; + + if (isCurrent) { + badge.textContent = "CURRENT VERSION"; + } else if (isFree) { + badge.textContent = "Included"; + } + + if (isCurrent || isFree) { + card.appendChild(badge); + } + + // Name + const name = document.createElement("h4"); + name.style.cssText = ` + margin: 0 0 10px 0; + color: #333; + font-size: 20px; + font-weight: 600; + `; + name.textContent = option.name || "Software Update"; + card.appendChild(name); + + // Version + const version = document.createElement("div"); + version.style.cssText = ` + color: #666; + font-size: 14px; + margin-bottom: 15px; + `; + version.innerHTML = ` Version: ${option.version || "N/A"}`; + card.appendChild(version); + + // Description + const desc = document.createElement("p"); + desc.style.cssText = ` + color: #555; + font-size: 14px; + line-height: 1.6; + margin: 0 0 20px 0; + flex-grow: 1; + `; + desc.textContent = option.description || "No description available"; + card.appendChild(desc); + + // Price section + const priceSection = document.createElement("div"); + priceSection.style.cssText = ` + border-top: 1px solid #e0e0e0; + padding-top: 15px; + margin-top: auto; + `; + + const priceText = document.createElement("div"); + priceText.style.cssText = ` + font-size: 24px; + font-weight: bold; + color: ${isCurrent ? '#6c757d' : (isFree ? '#04AA6D' : '#333')}; + margin-bottom: 15px; + `; + + if (isCurrent) { + priceText.textContent = "INSTALLED"; + } else { + priceText.textContent = isFree ? "Included" : `${option.currency || "€"} ${price.toFixed(2)}`; + } + + priceSection.appendChild(priceText); + + // Action button + const actionBtn = document.createElement("button"); + actionBtn.className = "btn"; + actionBtn.style.cssText = ` + width: 100%; + background: ${isCurrent ? '#6c757d' : '#04AA6D'}; + color: white; + border: none; + padding: 12px; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + cursor: ${isCurrent ? 'not-allowed' : 'pointer'}; + transition: background 0.3s ease; + opacity: ${isCurrent ? '0.5' : '1'}; + `; + + if (isCurrent) { + actionBtn.innerHTML = ' Currently Installed'; + actionBtn.disabled = true; + } else if (isFree) { + actionBtn.innerHTML = ''; + actionBtn.onclick = () => selectUpgrade(option); + actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a'; + actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D'; + } else { + actionBtn.innerHTML = ''; + actionBtn.onclick = () => selectUpgrade(option); + actionBtn.onmouseenter = () => actionBtn.style.background = '#038f5a'; + actionBtn.onmouseleave = () => actionBtn.style.background = '#04AA6D'; + } + + priceSection.appendChild(actionBtn); + + card.appendChild(priceSection); + grid.appendChild(card); + }); +} + +async function selectUpgrade(option) { + const price = parseFloat(option.price || 0); + const isFree = price === 0; + + // If paid upgrade, show payment modal first + if (!isFree) { + showPaymentModal(option); + return; + } + + // Free upgrade - show confirmation modal first + showFreeInstallModal(option); +} + +function showFreeInstallModal(option) { + // Create modal overlay + const modal = document.createElement("div"); + modal.id = "freeInstallModal"; + modal.style.cssText = ` + display: flex; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 1000; + align-items: center; + justify-content: center; + `; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.style.cssText = ` + background: white; + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + margin: 20px; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); + `; + + modalContent.innerHTML = ` +
+

Confirm Software Installation

+
+
+
+

${option.name || "Software Update"}

+

Version: ${option.version || "N/A"}

+

${option.description || ""}

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
+
+ +
+ + +
+
+
+ `; + + 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("freeInstallName").value = customerData.name; + if (customerData.email) document.getElementById("freeInstallEmail").value = customerData.email; + if (customerData.address) document.getElementById("freeInstallAddress").value = customerData.address; + if (customerData.city) document.getElementById("freeInstallCity").value = customerData.city; + if (customerData.postal) document.getElementById("freeInstallPostal").value = customerData.postal; + if (customerData.country) document.getElementById("freeInstallCountry").value = customerData.country; + } catch (e) { + console.warn('Error parsing saved customer data:', e); + } + } + + // Close modal on cancel + document.getElementById("cancelFreeInstall").onclick = () => { + document.body.removeChild(modal); + }; + + // Handle form submission + document.getElementById("freeInstallForm").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"), + version_id: option.version_id, + price: 0, + currency: option.currency || "€" + }; + + // Save customer data to sessionStorage for future use + sessionStorage.setItem('customerData', JSON.stringify({ + name: customerData.name, + email: customerData.email, + address: customerData.address, + city: customerData.city, + postal: customerData.postal, + country: customerData.country + })); + + // Close modal + document.body.removeChild(modal); + + // Proceed to download and install with customer data + await downloadAndInstallSoftware(option, customerData); + }; +} + +function showPaymentModal(option) { + const price = parseFloat(option.price || 0); + const currency = option.currency || "€"; + + // Create modal overlay + const modal = document.createElement("div"); + modal.id = "paymentModal"; + modal.style.cssText = ` + display: flex; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 1000; + align-items: center; + justify-content: center; + `; + + // Create modal content + const modalContent = document.createElement("div"); + modalContent.style.cssText = ` + background: white; + border-radius: 8px; + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; + margin: 20px; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); + `; + + modalContent.innerHTML = ` +
+

Purchase Software Upgrade

+
+
+
+

${option.name || "Software Update"}

+

Version: ${option.version || "N/A"}

+

${option.description || ""}

+
+ ${currency} ${price.toFixed(2)} +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ + +
+
+ +
+ + +
+ +
+ + +
+
+
+ `; + + 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("paymentName").value = customerData.name; + if (customerData.email) document.getElementById("paymentEmail").value = customerData.email; + 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; + } catch (e) { + console.warn('Error parsing saved customer data:', e); + } + } + + // Close modal on cancel + document.getElementById("cancelPayment").onclick = () => { + document.body.removeChild(modal); + }; + + // Handle form submission + document.getElementById("paymentForm").onsubmit = async (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + const paymentData = { + name: formData.get("name"), + email: formData.get("email"), + address: formData.get("address"), + city: formData.get("city"), + postal: formData.get("postal"), + country: formData.get("country"), + payment_method: formData.get("payment_method"), + version_id: option.version_id, + price: price, + currency: currency + }; + + // Save customer data to sessionStorage for future use + sessionStorage.setItem('customerData', JSON.stringify({ + name: paymentData.name, + email: paymentData.email, + address: paymentData.address, + city: paymentData.city, + postal: paymentData.postal, + country: paymentData.country + })); + + await processPayment(paymentData, option, modal); + }; +} + +async function logSoftwareInstallationToHistory(option, customerData = null) { + try { + const serviceToken = document.getElementById("servicetoken")?.innerHTML || ''; + + // Create payload matching the structure expected by v2/history + const payload = { + version: option.version, + version_id: option.version_id, + name: option.name, + description: option.description, + HW: deviceHwVersion, + HEX_FW: option.version, + price: option.price || 0, + currency: option.currency || "€", + installation_source: 'softwaretool' + }; + + // Add customer data if provided + if (customerData) { + payload.customer = { + name: customerData.name, + email: customerData.email, + address: customerData.address, + city: customerData.city, + postal: customerData.postal, + country: customerData.country + }; + } + + const historyData = { + sn: deviceSerialNumber, + type: 'firmware', + sn_service: 'Portal', + payload: payload + }; + + const url = link + '/v2/history'; + const bearer = 'Bearer ' + serviceToken; + + console.log("Logging installation to history:", historyData); + await logCommunication(`Logging software installation to history: ${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 log to history:', response.status); + await logCommunication(`Failed to log to history: ${response.status}`, 'error'); + } else { + const result = await response.json(); + console.log("History logged successfully:", result); + await logCommunication(`History logged successfully: ${JSON.stringify(result)}`, 'received'); + } + } catch (error) { + console.warn('Error logging to history:', error); + await logCommunication(`Error logging to history: ${error.message}`, 'error'); + // Don't block the installation if history logging fails + } +} + +async function processPayment(paymentData, option, modal) { + try { + progressBar("10", "Processing payment...", "#04AA6D"); + + // TODO: Call payment provider API + // For now, simulate payment processing + await logCommunication(`Payment initiated: ${JSON.stringify(paymentData)}`, 'sent'); + + // Simulate API call to payment provider + // const paymentResponse = await fetch(link + '/v2/payment/process', { + // method: 'POST', + // headers: { + // 'Authorization': 'Bearer ' + document.getElementById("servicetoken").textContent, + // 'Content-Type': 'application/json' + // }, + // body: JSON.stringify(paymentData) + // }); + + // For now, simulate success after 2 seconds + await new Promise(resolve => setTimeout(resolve, 2000)); + + await logCommunication(`Payment successful`, 'received'); + + // Close payment modal + document.body.removeChild(modal); + + // Proceed to download and install + await downloadAndInstallSoftware(option); + + } catch (error) { + await logCommunication(`Payment error: ${error.message}`, 'error'); + progressBar("0", "Payment failed: " + error.message, "#ff6666"); + alert("Payment failed: " + error.message); + } +} + +async function downloadAndInstallSoftware(option, customerData = null) { + selectedSoftwareUrl = option.source; + + if (!selectedSoftwareUrl) { + alert("Error: No download URL available"); + await logCommunication('No download URL available for selected option', 'error'); + return; + } + + console.log("Download URL:", selectedSoftwareUrl); + await logCommunication(`Downloading software from: ${selectedSoftwareUrl}`, 'sent'); + + // Log to v2/history before starting download (with customer data if provided) + await logSoftwareInstallationToHistory(option, customerData); + + try { + // Download the software file + progressBar("10", "Downloading software...", "#04AA6D"); + + const response = await fetch(selectedSoftwareUrl, { + method: "GET", + headers: { + "Authorization": "Bearer " + document.getElementById("servicetoken").textContent + } + }); + + if (!response.ok) { + // Try to parse error message + const errorText = await response.text(); + let errorMessage = `Download failed: ${response.status}`; + + try { + const errorJson = JSON.parse(errorText); + errorMessage = errorJson.message || errorJson.error || errorMessage; + } catch (e) { + // Not JSON, use status text + } + + console.error("Download failed:", response.status, errorText); + await logCommunication(`Download failed: ${response.status} - ${errorText}`, 'error'); + progressBar("0", "Error: " + errorMessage, "#ff6666"); + alert("Download failed: " + errorMessage); + return; + } + + // Get the blob + const blob = await response.blob(); + + progressBar("50", "Download complete, preparing installation...", "#04AA6D"); + await logCommunication(`Software downloaded successfully, size: ${blob.size} bytes`, 'received'); + + // Create a blob URL for the downloaded file + const blobUrl = URL.createObjectURL(blob); + + // Set global variables expected by upload.js + window.firmwarelocation = blobUrl; + window.upgraded_version = option.version || ""; + + // DEBUG MODE: Don't auto-trigger upload, let user manually test + if (typeof DEBUG !== 'undefined' && DEBUG) { + // Show upload section and button for manual testing + document.getElementById("uploadSection").style.display = "block"; + const uploadBtn = document.getElementById("uploadSoftware"); + uploadBtn.style.display = "block"; + uploadBtn.disabled = false; + + progressBar("100", "DEBUG MODE: Download complete. Click 'Install Software' button to test upload.js manually", "#ff9800"); + console.log("=== DEBUG MODE: Manual Upload Test ==="); + console.log("Firmware downloaded successfully"); + console.log("Blob size:", blob.size, "bytes"); + console.log("Blob URL:", blobUrl); + console.log("window.firmwarelocation =", window.firmwarelocation); + console.log("window.upgraded_version =", window.upgraded_version); + console.log("Upload button is now visible and enabled"); + 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 + document.getElementById("uploadSection").style.display = "block"; + const uploadBtn = document.getElementById("uploadSoftware"); + uploadBtn.style.display = "block"; + uploadBtn.disabled = false; + + progressBar("60", "Ready to install, starting upload...", "#04AA6D"); + uploadBtn.click(); + } + + } catch (error) { + await logCommunication(`Download error: ${error.message}`, 'error'); + progressBar("0", "Error downloading software: " + error.message, "#ff6666"); + alert("Error: " + error.message); + } +} diff --git a/communication.php b/communication.php index 9dd632d..9fb97e3 100644 --- a/communication.php +++ b/communication.php @@ -113,9 +113,7 @@ $view .= ''; $view .= '
'.$tab1.' - '.$tab3.' -
- '; + '; //GET PARTNERID $view_partners = ''; @@ -162,6 +160,9 @@ $view .= '
'; +$view .= '
+ '.$tab3.' +
'; $view .= '
diff --git a/dealer.php b/dealer.php index 504b682..fbaa36f 100644 --- a/dealer.php +++ b/dealer.php @@ -125,24 +125,32 @@ $view .= '
'.(${$responses['short_description']} ?? $responses['short_description']).'
+
'.(${$responses['long_description']} ?? $responses['long_description']).'
+
'.(${$responses['usp1']} ?? $responses['usp1']).'
+
'.(${$responses['usp2']} ?? $responses['usp2']).'
+
'.(${$responses['usp3']} ?? $responses['usp3']).'
diff --git a/equipment_data.php b/equipment_data.php index 69b9f40..174f861 100644 --- a/equipment_data.php +++ b/equipment_data.php @@ -49,9 +49,7 @@ $view .= '
'; $view .= '
'.$tab1.' - '.$tab3.' -
- '; +
'; $view .= '
@@ -96,8 +94,11 @@ $view .= '
} $view .= '
-
'; + +
'; $view .= '
diff --git a/firmwaretool.php b/firmwaretool.php index 4cdb3e2..928fd17 100644 --- a/firmwaretool.php +++ b/firmwaretool.php @@ -26,8 +26,8 @@ $view = '

'.$firmwaretool_h2 .'

'.$firmwaretool_p.'

-
-'; + '; + if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['equipmentID']; } else {$returnpage = 'dashboard';} @@ -35,12 +35,15 @@ if (isset($_GET['equipmentID'])){$returnpage = 'equipment&equipmentID='.$_GET['e //SHOW BACK BUTTON ONLY FOR PORTAL USERS if (isAllowed('dashboard',$_SESSION['profile'],$_SESSION['permission'],'R') != 0){ $view .= ' -
- +
+
'; } +$view .= ' +
'; + $view .= '
diff --git a/index.php b/index.php index a640646..a6d652d 100644 --- a/index.php +++ b/index.php @@ -82,7 +82,13 @@ if (isset($_GET['page']) && $_GET['page'] == 'logout') { //===================================== $allowed_views = explode(',',$_SESSION['profile']); $ignoreViews = ['profile','assets','sales']; -$allowed_views = findExistingView($allowed_views, 'dashboard', $ignoreViews); + +// If dashboard is in the profile, prioritize it +if (in_array('dashboard', $allowed_views) && file_exists('dashboard.php')) { + $allowed_views = 'dashboard'; +} else { + $allowed_views = findExistingView($allowed_views, 'dashboard', $ignoreViews); +} //===================================== //FORWARD THE USER TO THE CORRECT PAGE diff --git a/language.php b/language.php index 9e754dc..1837706 100644 --- a/language.php +++ b/language.php @@ -45,7 +45,6 @@ if (isset($_GET['success_msg'])) {
US - NL
@@ -53,7 +52,10 @@ if (isset($_GET['success_msg'])) {
-
+
+ NL +
+
diff --git a/logfile.php b/logfile.php index 39786a1..a9af451 100644 --- a/logfile.php +++ b/logfile.php @@ -147,7 +147,6 @@ if (file_exists($filelocation_webserver)){
@@ -156,6 +155,9 @@ if (file_exists($filelocation_webserver)){
+
+ Webserver +
diff --git a/maintenance.php b/maintenance.php index 6ffea24..8b66507 100644 --- a/maintenance.php +++ b/maintenance.php @@ -70,8 +70,6 @@ $view .=' $view .= ' '; @@ -96,6 +94,11 @@ $view .= '
'; } +$view .= '
+ Learning +
+ '; + if ($update_allowed === 1){ $view .= '
@@ -109,6 +112,11 @@ if ($update_allowed === 1){
'; } +$view .= ' + '; + if ($update_allowed === 1){ $view .= '
diff --git a/partner.php b/partner.php index 89693c9..13f82ea 100644 --- a/partner.php +++ b/partner.php @@ -120,10 +120,7 @@ $view .= '
'; $view .= ' - '; +
'; //Define Service and partner enabled $view .= '
@@ -163,6 +160,9 @@ $salesid_dropdown = listPartner('salesid',$_SESSION['permission'],$partner_data- $soldto_dropdown = listPartner('soldto',$_SESSION['permission'],$partner_data->soldto,''); //DISPLAY +$view .= '
+ '.$tab2.' +
'; $view .= '
'; @@ -178,6 +178,9 @@ if ($_SESSION['permission'] == 3 || $_SESSION['permission'] == 4){
'; +$view .= '
+ '.$tab3.' +
'; $view .= '
diff --git a/products_configurations.php b/products_configurations.php index a736d21..f00c524 100644 --- a/products_configurations.php +++ b/products_configurations.php @@ -141,9 +141,7 @@ $view .= '
'; $view .= ' - '; +
'; $view .= '
@@ -171,6 +169,9 @@ $view .= '
'; +$view .= '
+ '.$tab3.' +
'; $view .= '
diff --git a/products_software.php b/products_software.php index 8211b86..6f396ea 100644 --- a/products_software.php +++ b/products_software.php @@ -188,7 +188,6 @@ $view .= '
'; $view .= ' '; @@ -233,6 +232,11 @@ $view .= '
'; +$view .= '
+ '.$tab3.' +
+ '; + $view .= '
diff --git a/products_software_upgrade_paths_manage.php b/products_software_upgrade_paths_manage.php index 0b2380d..592a0be 100644 --- a/products_software_upgrade_paths_manage.php +++ b/products_software_upgrade_paths_manage.php @@ -37,12 +37,19 @@ $path = [ 'updatedby' => $_SESSION['username'] ]; -// Determine filter version id from URL (for hw_version filtering) -$filter_version_id = $_GET['from_version_id'] ?? $_GET['to_version_id'] ?? $_GET['id'] ?? ''; +// Check if coming from version page (id parameter) or editing existing path +$from_version_page = false; +$to_version_fixed = false; +if (isset($_GET['id']) && !isset($_GET['path_id'])) { + // Coming from version page - this is the TO version + $from_version_page = true; + $to_version_fixed = $_GET['id']; + $path['to_version_id'] = $to_version_fixed; +} -// If editing, fetch existing data -if (isset($_GET['id']) && $_GET['id'] != '') { - $api_url = '/v2/products_software_upgrade_paths/rowID=' . $_GET['id']; +// If editing an existing path, load it +if (isset($_GET['path_id']) && $_GET['path_id'] != '') { + $api_url = '/v2/products_software_upgrade_paths/rowID=' . $_GET['path_id']; $response = ioServer($api_url, ''); if (!empty($response)) { @@ -53,6 +60,9 @@ if (isset($_GET['id']) && $_GET['id'] != '') { } } +// Determine filter version id from URL (for hw_version filtering) +$filter_version_id = $_GET['from_version_id'] ?? $_GET['to_version_id'] ?? $_GET['id'] ?? ''; + // Fetch software versions for selects $api_url = '/v2/products_software_versions/list'; $versions_response = ioServer($api_url, ''); @@ -163,10 +173,10 @@ $view =' ' . $button_cancel . ' '; -if ($delete_allowed === 1 && isset($_GET['id'])){ +if ($delete_allowed === 1 && isset($_GET['path_id']) && $_GET['path_id'] != ''){ $view .= ''; } -if (($update_allowed === 1 && isset($_GET['id'])) || ($create_allowed === 1 && !isset($_GET['id']))){ +if (($update_allowed === 1 && isset($_GET['path_id'])) || ($create_allowed === 1 && !isset($_GET['path_id']))){ $view .= ''; } @@ -179,21 +189,48 @@ $view .= '
'; if (!empty($versions)) { foreach ($versions as $ver) { + // Skip the TO version from FROM dropdown to prevent FROM = TO + if ($path['to_version_id'] && $ver->rowID == $path['to_version_id']) { + continue; + } $selected = ($path['from_version_id'] == $ver->rowID) ? ' selected' : ''; $view .= ''; } } -$view .= ' +$view .= ' '; + +// If TO version is fixed (coming from version page), show it as read-only text +if ($from_version_page && $to_version_fixed) { + $to_version_name = ''; + foreach ($versions as $ver) { + if ($ver->rowID == $to_version_fixed) { + $to_version_name = htmlspecialchars($ver->name . ' (' . $ver->version . ')'); + break; + } + } + $view .= ' + + + '; +} else { + // Show dropdown for TO version when editing + $view .= ' + $view .= ' '; +} +$view .= ' @@ -207,6 +244,30 @@ $view .= '
+ + '; //OUTPUT diff --git a/products_software_version.php b/products_software_version.php index eb00750..5c5bb7f 100644 --- a/products_software_version.php +++ b/products_software_version.php @@ -154,7 +154,7 @@ $view = ' } else { foreach ($all_paths as $path){ $view .= ' - + ' . ($version_map[$path->from_version_id] ?? $path->from_version_id) . ' ' . ($version_map[$path->to_version_id] ?? $path->to_version_id) . ' '.$path->price.' diff --git a/products_versions.php b/products_versions.php index f1b6e0d..d9cd839 100644 --- a/products_versions.php +++ b/products_versions.php @@ -110,7 +110,6 @@ $view .= '
'; $view .= ' '; @@ -185,6 +184,11 @@ $view .= '
'; +$view .= '
+ '.$tab3.' +
+ '; + $view .= '
diff --git a/profile.php b/profile.php index 66aa7ab..f94e321 100644 --- a/profile.php +++ b/profile.php @@ -93,9 +93,7 @@ $view .= '
'; $view .= ' - '; +
'; //Define Service and User enabled $view .= '
@@ -130,6 +128,10 @@ $view .=' $view .= '
'; +$view .= '
+ '.$tab3.' +
'; + $view .= '
diff --git a/settings.php b/settings.php index b6b5cce..9dd34f7 100644 --- a/settings.php +++ b/settings.php @@ -59,38 +59,33 @@ function format_var_html($key, $value) { $html .= ''; return $html; } -// Format tabs -function format_tabs($contents) { +// Format tabs and content together (interleaved for collapsible functionality) +function format_tabs_and_content($contents) { $rows = explode("\n", $contents); - $tab = '
'; - $tab .= 'General'; + $output = ''; + + // Start with General tab and its content + $output .= ''; + $output .= '
'; + for ($i = 0; $i < count($rows); $i++) { preg_match('/\/\*(.*?)\*\//', $rows[$i], $match); if ($match) { - $tab .= '' . $match[1] . ''; + // Close previous content and start new tab + $output .= '
'; + $output .= ''; + $output .= '
'; + } + preg_match('/define\(\'(.*?)\', ?(.*?)\)/', $rows[$i], $define_match); + if ($define_match) { + $output .= format_var_html($define_match[1], $define_match[2]); } } - $tab .= '
'; - return $tab; + $output .= '
'; + + return $output; } -// Format form -function format_form($contents) { - $rows = explode("\n", $contents); - $form = '
'; - for ($i = 0; $i < count($rows); $i++) { - preg_match('/\/\*(.*?)\*\//', $rows[$i], $match); - if ($match) { - $form .= '
'; - } - preg_match('/define\(\'(.*?)\', ?(.*?)\)/', $rows[$i], $match); - if ($match) { - $form .= format_var_html($match[1], $match[2]); - } - } - $form .= '
'; - return $form; -} if (isset($_POST['submit']) && !empty($_POST)) { // Update the configuration file with the new keys and values foreach ($_POST as $k => $v) { @@ -98,9 +93,7 @@ if (isset($_POST['submit']) && !empty($_POST)) { $contents = preg_replace('/define\(\'' . $k . '\'\, ?(.*?)\)/s', 'define(\'' . $k . '\',' . $v . ')', $contents); } file_put_contents($file, $contents); - - //Return succesmessage header('Location: index.php?page=settings&success_msg=1'); exit; @@ -144,18 +137,9 @@ if (isset($success_msg)){
'; } -$view .= format_tabs($contents); -$view .= '
-
- '; -$view .= format_form($contents); +$view .= format_tabs_and_content($contents); $view .= ' -
-
- - - + +'; + +template_footer(); +?> diff --git a/style/admin.css b/style/admin.css index c05d66f..8988e13 100644 --- a/style/admin.css +++ b/style/admin.css @@ -1002,8 +1002,7 @@ main .manage-order-table .delete-item:hover { .table { overflow-x: auto; padding: 0; - border-radius: 8px; - overflow: hidden; + border-radius: 4px; } .table table { diff --git a/user.php b/user.php index 6b6d64d..1dd55dd 100644 --- a/user.php +++ b/user.php @@ -185,11 +185,7 @@ $view .= '
'; $view .= '
'.$tab1 .' - '.$tab2.' - '.$tab3.' - '.(($update_allowed === 1 && $user_ID !='')? ''.$general_actions.'':"").' -
- '; +
'; //Define Service and User enabled $view .= '
@@ -263,6 +259,10 @@ $view .=' $view .= '
'; +$view .= '
+ '.$tab2.' +
'; + //GET PARTNERDATA $partner_data = json_decode($user['partnerhierarchy'])?? json_decode($_SESSION['partnerhierarchy']) ; //BUID UP DROPDOWNS @@ -289,6 +289,10 @@ $view .= ' '; +$view .= '
+ '.$tab3.' +
'; + //SUPERUSERS AND ADMINS CAN RESET BLOCKED USERS if ($_SESSION['permission'] == 3 || $_SESSION['permission'] == 4){ @@ -313,6 +317,9 @@ $view .= '
'; if ($update_allowed === 1 && $user_ID !=''){ +$view .= '
+ '.$general_actions.' +
'; $view .= '