diff --git a/public/js/pages/Radius/Radius.js b/public/js/pages/Radius/Radius.js index bb57045ca..94ad6773a 100644 --- a/public/js/pages/Radius/Radius.js +++ b/public/js/pages/Radius/Radius.js @@ -324,6 +324,7 @@ Vue.component('radius', { +
@@ -411,6 +412,10 @@ Vue.component('radius', {
+ +
+ +
@@ -533,3 +538,372 @@ 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 }}
+
+
+
+ `, + + data() { + return { + step: 1, // 1: Upload, 2: Results + parsedData: [], // Raw data from XLSX + processedData: [], // Data after API calls + originalHeaders: [], // Headers from the uploaded XLSX + 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 + 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 + }; + }, + + 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) + const input = this.$el.querySelector('input[type="file"]'); + 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 + if (!file) return; + + this.loading = true; // Show loading indicator early + + try { + // Load XLSX library dynamically if not already loaded + await this.loadXLSX(); + + 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]; + + // 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 === 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.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); + }); + } + }, + + /** + * 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.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'; + + for (let i = 0; i < this.parsedData.length; i++) { + 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] = ''; + + 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 + } + + 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(); + + 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'; + } + } + } 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'; + } + + this.processedData.push(row); + + // 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); + } + } + + this.loading = false; + this.step = 2; // Move to results view + this.currentSerial = ''; // Clear serial display + }, + + /** + * 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 + exportRow['Username'] = row[this.fetchedKeys.username]; + exportRow['Kundennummer'] = row[this.fetchedKeys.customerNumber]; + exportRow['Kundenname'] = row[this.fetchedKeys.customerName]; + exportRow['Info'] = row[this.fetchedKeys.info]; + 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) + const timestamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14); + const filename = `ont_finder_results_${timestamp}.xlsx`; + + XLSX.writeFile(wb, filename); + } 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 + } + }, + + /** + * 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']; + } + // 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)); + } +});