diff --git a/public/js/pages/Radius/Radius.js b/public/js/pages/Radius/Radius.js index fd54a7794..6ed319219 100644 --- a/public/js/pages/Radius/Radius.js +++ b/public/js/pages/Radius/Radius.js @@ -554,321 +554,245 @@ Vue.component('radius', { Vue.component('radius-ont-finder', { template: ` -
-
-
-

Schritt 1: Excel (XLSX) Upload

-

Bitte laden Sie eine XLSX-Datei hoch, die mindestens eine Spalte mit dem Header 'Serial' für die ONT-Seriennummern enthält. Erwartete Spalten (optional, aber zur Anzeige empfohlen): Nummer, Serial, ONT-Type, Meter, Pegel.

- -
{{ uploadError }}
-
-
- -
-
-

Schritt 2: Ergebnisse

- - - -
- - - - - - - - - - - - - - - - - - - -
{{ header }}UsernameKundennummerKundennameInfo
{{ row[header] }}{{ row.fetched_username }}{{ row.fetched_customerNumber }}{{ row.fetched_customerName }}{{ row.fetched_info }}
-
-
-
- -
-
-
- Loading... -
-
-
- {{ Math.round(progress) }}% -
-
-
Verarbeite Zeile {{ currentRow + 1 }} von {{ totalRows }}
-
Aktuelle ONT SN: {{ currentSerial }}
-
-
+
+
+
+

Schritt 1: Excel (XLSX) Upload

+

Bitte laden Sie eine XLSX-Datei hoch, die mindestens eine Spalte mit dem Header 'Serial' für die ONT-Seriennummern enthält. Optional kann eine 'MAC' Spalte für eine alternative Suche verwendet werden.

+ +
{{ uploadError }}
+
+ +
+
+

Schritt 2: Ergebnisse

+ + +
+ + + + + + + + + + + + + + + + + + + +
{{ header }}UsernameKundennummerKundennameInfo
{{ row[header] }}{{ row.fetched_username }}{{ row.fetched_customerNumber }}{{ row.fetched_customerName }}{{ row.fetched_info }}
+
+
+
+ +
+
+
+ Loading... +
+
+
+ {{ Math.round(progress) }}% +
+
+
Verarbeite Zeile {{ currentRow + 1 }} von {{ totalRows }}
+
Aktuelle Suche: {{ currentSerial }}
+
+
+
`, data() { return { - step: 1, // 1: Upload, 2: Results - parsedData: [], // Raw data from XLSX - processedData: [], // Data after API calls - originalHeaders: [], // Headers from the uploaded XLSX + step: 1, + parsedData: [], + processedData: [], + originalHeaders: [], loading: false, progress: 0, currentRow: 0, totalRows: 0, - currentSerial: '', // Track the serial being processed for display - uploadError: null, // To display errors during upload/parsing - // Define the key column name expected in the XLSX for the ONT Serial Number - serialColumnName: 'Serial', // IMPORTANT: Adjust if the header name in the XLSX is different - // Define keys for the fetched data to avoid conflicts with original headers + currentSerial: '', + uploadError: null, + serialColumnName: 'Serial', + macColumnName: 'MAC', fetchedKeys: { username: 'fetched_username', customerNumber: 'fetched_customerNumber', customerName: 'fetched_customerName', info: 'fetched_info' }, - // Base path for the API - ensure TT_CONFIG is available globally - apiBasePath: window.TT_CONFIG ? window.TT_CONFIG['BASE_PATH'] : '/default/path/to/api' // Provide a fallback or handle error if TT_CONFIG is missing + apiBasePath: window.TT_CONFIG?.BASE_PATH }; }, methods: { - /** - * Resets the component state to allow a new file upload. - */ resetComponent() { - this.step = 1; - this.parsedData = []; - this.processedData = []; - this.originalHeaders = []; - this.loading = false; - this.progress = 0; - this.currentRow = 0; - this.totalRows = 0; - this.currentSerial = ''; - this.uploadError = null; - // Reset the file input visually (optional, requires ref) + Object.assign(this.$data, this.$options.data.call(this)); const input = this.$el.querySelector('input[type="file"]'); - if (input) { - input.value = ''; - } + if (input) input.value = ''; }, - /** - * Handles the file input change event. - * Loads the XLSX library if needed, reads the file, - * parses the data, validates the 'Serial' column, - * and triggers processing. - * @param {Event} event - The file input change event. - */ async handleFileUpload(event) { const file = event.target.files[0]; - this.uploadError = null; // Clear previous errors + this.uploadError = null; if (!file) return; - this.loading = true; // Show loading indicator early + this.loading = true; try { - // Load XLSX library dynamically if not already loaded await this.loadXLSX(); + const data = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = e => resolve(new Uint8Array(e.target.result)); + reader.onerror = () => reject(new Error("Fehler beim Lesen der Datei.")); + reader.readAsArrayBuffer(file); + }); - const reader = new FileReader(); - reader.onload = (e) => { - try { - const data = new Uint8Array(e.target.result); - const workbook = XLSX.read(data, { type: "array" }); - const worksheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[worksheetName]; + const workbook = XLSX.read(data, { type: 'array' }); + const worksheet = workbook.Sheets[workbook.SheetNames[0]]; + this.parsedData = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); - // Parse the sheet into an array of objects, automatically detecting headers - this.parsedData = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); // Use defval to handle empty cells + if (!this.parsedData.length) { + throw new Error("Die hochgeladene Datei ist leer oder konnte nicht gelesen werden."); + } - if (this.parsedData.length === 0) { - this.uploadError = "Die hochgeladene Datei ist leer oder konnte nicht gelesen werden."; - this.loading = false; - return; - } - - // Get headers from the first row of parsed data - this.originalHeaders = Object.keys(this.parsedData[0]); - - // --- Validation: Check if the required 'Serial' column exists --- - if (!this.originalHeaders.includes(this.serialColumnName)) { - this.uploadError = `Fehler: Die erforderliche Spalte '${this.serialColumnName}' wurde in der hochgeladenen Datei nicht gefunden. Gefundene Spalten: ${this.originalHeaders.join(', ')}`; - this.loading = false; - this.parsedData = []; // Clear data if invalid - this.originalHeaders = []; - // Keep step at 1 to show the error - return; - } - // --- End Validation --- - - // If validation passes, proceed to processing - this.startProcessing(); - - } catch (parseError) { - console.error("Error parsing XLSX file:", parseError); - this.uploadError = `Fehler beim Verarbeiten der XLSX-Datei: ${parseError.message}`; - this.loading = false; - this.step = 1; // Stay on upload step - } - }; - reader.onerror = (err) => { - console.error("FileReader error:", err); - this.uploadError = "Fehler beim Lesen der Datei."; - this.loading = false; - this.step = 1; - }; - reader.readAsArrayBuffer(file); - - } catch (libLoadError) { - console.error("Error loading XLSX library:", libLoadError); - this.uploadError = "Fehler beim Laden der erforderlichen Bibliothek (xlsx). Bitte versuchen Sie es erneut."; + this.originalHeaders = Object.keys(this.parsedData[0]); + if (!this.originalHeaders.includes(this.serialColumnName)) { + throw new Error(`Erforderliche Spalte '${this.serialColumnName}' nicht gefunden.`); + } + this.startProcessing(); + } catch (error) { + console.error("File processing error:", error); + this.uploadError = error.message; this.loading = false; this.step = 1; } }, - /** - * Dynamically loads the SheetJS (XLSX) library if it's not already available. - */ async loadXLSX() { - if (!window.XLSX) { - console.log("Loading XLSX library..."); - await new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'; // Consider using a newer version if available - script.async = true; - script.onload = () => { - console.log("XLSX library loaded."); - resolve(); - }; - script.onerror = (err) => { - console.error("Failed to load XLSX script:", err); - reject(new Error("Could not load XLSX library")); - }; - document.head.appendChild(script); - }); - } + if (window.XLSX) return; + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'; + script.async = true; + script.onload = resolve; + script.onerror = () => reject(new Error("Could not load XLSX library.")); + document.head.appendChild(script); + }); }, - /** - * Processes the parsed data row by row, fetching details from the Radius API. - */ async startProcessing() { this.loading = true; this.totalRows = this.parsedData.length; - this.processedData = []; // Clear previous results + this.processedData = []; this.progress = 0; this.currentRow = 0; - this.currentSerial = ''; - const apiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?ont_sn=`; - const notFoundMessage = 'N/A - Keinen Benutzer mit dieser ONT SN gefunden'; + const snApiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?ont_sn=`; + const macApiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?username=`; + const sesApiUrlBase = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?action2=find_by_current_session&mac=`; - for (let i = 0; i < this.parsedData.length; i++) { + const setRowStatus = (row, msg, data = {}) => { + const defaultData = { username: `N/A - ${msg}`, customerNumber: 'N/A', customerName: 'N/A', info: 'N/A' }; + Object.keys(this.fetchedKeys).forEach(key => row[this.fetchedKeys[key]] = data[key] || defaultData[key]); + }; + + for (const [i, row] of this.parsedData.entries()) { this.currentRow = i; - const row = { ...this.parsedData[i] }; // Create a copy to avoid modifying original parsed data - const serialNumber = row[this.serialColumnName]?.trim(); // Get serial number, trim whitespace - - this.currentSerial = serialNumber || 'Leer'; // Update display - this.progress = ((i + 1) / this.totalRows) * 100; - - // Initialize fetched data fields - row[this.fetchedKeys.username] = ''; - row[this.fetchedKeys.customerNumber] = ''; - row[this.fetchedKeys.customerName] = ''; - row[this.fetchedKeys.info] = ''; + const newRow = { ...row }; + const serialNumber = row[this.serialColumnName]?.trim(); + this.currentSerial = `SN: ${serialNumber || 'Leer'}`; if (!serialNumber) { - // Handle rows with empty serial numbers - row[this.fetchedKeys.username] = 'N/A - Leere Seriennummer'; - row[this.fetchedKeys.customerNumber] = 'N/A'; - row[this.fetchedKeys.customerName] = 'N/A'; - row[this.fetchedKeys.info] = 'N/A'; - this.processedData.push(row); - await this.sleep(10); // Small delay for UI update even for empty rows - continue; // Move to the next row + setRowStatus(newRow, 'Leere Seriennummer'); + this.processedData.push(newRow); + this.progress = ((i + 1) / this.totalRows) * 100; + continue; } - try { - const response = await fetch(apiUrlBase + encodeURIComponent(serialNumber)); - if (!response.ok) { - // Handle HTTP errors (e.g., 404, 500) - console.error(`API Error for SN ${serialNumber}: ${response.status} ${response.statusText}`); - row[this.fetchedKeys.username] = `N/A - API Fehler (${response.status})`; - row[this.fetchedKeys.customerNumber] = 'N/A'; - row[this.fetchedKeys.customerName] = 'N/A'; - row[this.fetchedKeys.info] = 'N/A'; - } else { - const data = await response.json(); + let found = false; - if (Array.isArray(data) && data.length > 0) { - // Assuming the first result is the relevant one if multiple are returned - const userData = data[0]; - row[this.fetchedKeys.username] = userData.username || 'N/A'; - row[this.fetchedKeys.customerNumber] = userData.customerNumber || 'N/A'; - row[this.fetchedKeys.customerName] = userData.customerName || 'N/A'; - row[this.fetchedKeys.info] = userData.info || 'N/A'; - } else { - // Handle case where API returns success but an empty array or unexpected format - row[this.fetchedKeys.username] = notFoundMessage; - row[this.fetchedKeys.customerNumber] = 'N/A'; - row[this.fetchedKeys.customerName] = 'N/A'; - row[this.fetchedKeys.info] = 'N/A'; + try { + const snResponse = await fetch(snApiUrlBase + encodeURIComponent(serialNumber)); + if (snResponse.ok) { + const snData = await snResponse.json(); + if (snData?.length > 0) { + setRowStatus(newRow, '', snData[0]); + found = true; } } } catch (error) { - console.error(`Error fetching data for SN ${serialNumber}:`, error); - row[this.fetchedKeys.username] = 'N/A - Fehler bei API-Abfrage'; - row[this.fetchedKeys.customerNumber] = 'N/A'; - row[this.fetchedKeys.customerName] = 'N/A'; - row[this.fetchedKeys.info] = 'N/A'; + console.error(`Fetch error for SN ${serialNumber}:`, error); } - this.processedData.push(row); + if (!found && this.originalHeaders.includes(this.macColumnName)) { + const macAddress = row[this.macColumnName]?.trim(); + this.currentSerial = `MAC: ${macAddress || 'Leer'}`; + if (macAddress && macAddress.length === 12) { + const formattedMac = macAddress.toUpperCase().match(/.{1,2}/g).join(':'); + try { + const sesResponse = await fetch(`${sesApiUrlBase}${encodeURIComponent(formattedMac)}`); + if (sesResponse.ok) { + const sesData = await sesResponse.json(); + if (sesData?.length === 0) continue; - // Optional small delay to prevent UI freeze on large files and allow progress update - if (i % 20 === 0) { // Update UI roughly every 20 rows - await this.sleep(20); + const username = sesData[0]; + + const macResponse = await fetch(`${macApiUrlBase}${encodeURIComponent(username)}&info=&custnum=`); + if (macResponse.ok) { + const macData = await macResponse.json(); + if (macData?.length > 0) { + setRowStatus(newRow, '', macData[0]); + console.log("found via MAC:", formattedMac, macData[0]); + found = true; + } + } + } + + + + } catch (error) { + console.error(`Fetch error for MAC ${formattedMac}:`, error); + } + } } + + if (!found) { + setRowStatus(newRow, 'Keinen Benutzer gefunden'); + } + + this.processedData.push(newRow); + this.progress = ((i + 1) / this.totalRows) * 100; + if ((i + 1) % 20 === 0) await this.sleep(20); } this.loading = false; - this.step = 2; // Move to results view - this.currentSerial = ''; // Clear serial display + this.step = 2; + this.currentSerial = ''; }, - /** - * Creates and triggers the download of an XLSX file containing the processed results. - */ downloadResults() { if (!this.processedData.length) return; - try { - // Prepare data for export: Select and order columns const dataToExport = this.processedData.map(row => { const exportRow = {}; - // Include original columns first - this.originalHeaders.forEach(header => { - exportRow[header] = row[header]; - }); - // Add fetched data with user-friendly headers + this.originalHeaders.forEach(header => { exportRow[header] = row[header]; }); exportRow['Username'] = row[this.fetchedKeys.username]; exportRow['Kundennummer'] = row[this.fetchedKeys.customerNumber]; exportRow['Kundenname'] = row[this.fetchedKeys.customerName]; @@ -876,47 +800,25 @@ Vue.component('radius-ont-finder', { return exportRow; }); - const ws = XLSX.utils.json_to_sheet(dataToExport); const wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, "ONT_Finder_Results"); // Sheet name - - // Generate filename (e.g., results_YYYYMMDD_HHMMSS.xlsx) + XLSX.utils.book_append_sheet(wb, ws, "ONT_Finder_Results"); const timestamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14); - const filename = `ont_finder_results_${timestamp}.xlsx`; - - XLSX.writeFile(wb, filename); + XLSX.writeFile(wb, `ont_finder_results_${timestamp}.xlsx`); } catch (error) { console.error("Error generating results file:", error); - alert("Fehler beim Erstellen der Excel-Datei für den Download."); // Simple alert for user feedback + alert("Fehler beim Erstellen der Excel-Datei für den Download."); } }, - /** - * Utility function to pause execution for a specified duration. - * Useful for allowing UI updates during long loops. - * @param {number} ms - Milliseconds to sleep. - */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); - } + }, }, - /** - * Lifecycle hook called when the component is mounted. - * Checks if the global TT_CONFIG is available. - */ mounted() { - if (!window.TT_CONFIG || !window.TT_CONFIG['BASE_PATH']) { - console.warn("Global TT_CONFIG or TT_CONFIG['BASE_PATH'] not found. API calls may fail. Using fallback path:", this.apiBasePath); - // Optionally display a warning to the user - // this.uploadError = "Konfiguration für API-Pfad nicht gefunden. Funktionalität möglicherweise beeinträchtigt."; - } else { - // Update apiBasePath if TT_CONFIG was found after initial data setup (less likely but safe) - this.apiBasePath = window.TT_CONFIG['BASE_PATH']; + if (!window.TT_CONFIG?.BASE_PATH) { + console.warn(`Global TT_CONFIG.BASE_PATH not found. API calls will use fallback path: ${this.apiBasePath}`); } - // Ensure XLSX is loaded once the component is ready, in case it's needed immediately - // Although handleFileUpload loads it, pre-loading might be slightly smoother if needed elsewhere later - // this.loadXLSX().catch(err => console.error("Pre-loading XLSX library failed:", err)); } });