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
-
- Ergebnisse herunterladen
-
-
- Neue Datei hochladen
-
-
-
-
-
-
- {{ header }}
- Username
- Kundennummer
- Kundenname
- Info
-
-
-
-
- {{ 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
+
+ Ergebnisse herunterladen
+
+
+ Neue Datei hochladen
+
+
+
+
+
+ {{ header }}
+ Username
+ Kundennummer
+ Kundenname
+ Info
+
+
+
+
+ {{ 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));
}
});