// Function to calculate similarity percentage between two strings function calculateSimilarity(str1, str2) { // Normalize strings by converting them to lowercase str1 = str1.toLowerCase(); str2 = str2.toLowerCase(); let matchCount = 0; // Check how many characters in str1 exist in str2 for (let char of str1) { if (str2.includes(char)) { matchCount++; } } // Calculate similarity percentage return (matchCount / str1.length) * 100; } function validateData(strasse, plz, stadt, info) { const thresholds = 90; // Similarity threshold in percentage // Validate each field against the info string return !(calculateSimilarity(strasse, info) < thresholds || calculateSimilarity(plz, info) < thresholds || calculateSimilarity(stadt, info) < thresholds); } Vue.component('radius-ont-parser', { template: `

Schritt 1: Excel (XLSX) Upload

Schritt 2: Spaltenzuordnung

Schritt 3: Ergebnisse

ONT SN
{{ row[selectedColumns.kundennummer] }} {{ row[selectedColumns.anschlussstrasse] }} {{ row[selectedColumns.anschlussplz] }} {{ row[selectedColumns.anschlusscity] }} {{ row.ont_sn }}
{{ Math.round(progress) }}%
Processing {{ currentRow + 1 }} of {{ totalRows }}
`, data() { return { step: 1, headers: [], parsedData: [], processedData: [], selectedColumns: { kundennummer: 'crmPartner', anschlussstrasse: 'AnlStrasse', anschlussplz: 'AnlPlz', anschlusscity: 'AnlOrt' }, requiredFields: [ { key: 'kundennummer', label: 'Kundennummer' }, { key: 'anschlussstrasse', label: 'Anschlussstraße' }, { key: 'anschlussplz', label: 'Anschluss PLZ' }, { key: 'anschlusscity', label: 'Anschluss City' } ], loading: false, progress: 0, currentRow: 0, totalRows: 0 }; }, methods: { async handleFileUpload(event) { const file = event.target.files[0]; if (!file) return; // Load XLSX library dynamically await this.loadXLSX(); const reader = new FileReader(); reader.onload = (e) => { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: "array" }); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; // Read entire sheet as rows of arrays (no header detection yet) const allRows = XLSX.utils.sheet_to_json(worksheet, { header: 1, // Return rows as arrays blankrows: false // Skip blank rows }); // If there's only one row or something unexpected, do a basic parse if (allRows.length < 2) { const fallbackData = XLSX.utils.sheet_to_json(worksheet); this.parsedData = fallbackData; this.headers = Object.keys(fallbackData[0] || {}); this.step = 2; return; } const firstRow = allRows[0] || []; const secondRow = allRows[1] || []; // Count how many cells in each row are empty const firstRowEmptyCount = firstRow.length - firstRow.filter(Boolean).length; const secondRowEmptyCount = secondRow.length - secondRow.filter(Boolean).length; // If the difference in empty cells is more than 25% of the number of columns in the second row, // assume the first row is mostly empty and use the second row as the header const useSecondRowAsHeader = (firstRowEmptyCount - secondRowEmptyCount) > 0.25 * secondRow.length; // Now parse again with the correct header row if (useSecondRowAsHeader) { this.parsedData = XLSX.utils.sheet_to_json(worksheet, { range: 1, // Start reading data after the first row header: secondRow, // Use the second row as the header defval: "" // Optional: fill empty cells with empty string }).slice(1); // Skip the first row this.headers = secondRow; } else { this.parsedData = XLSX.utils.sheet_to_json(worksheet, { range: 0, // Start at the first row header: firstRow, // Use the first row as the header defval: "" }); this.headers = firstRow; } this.step = 2; }; reader.readAsArrayBuffer(file); }, async loadXLSX() { if (!window.XLSX) { 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'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } }, async startProcessing() { this.loading = true; this.totalRows = this.parsedData.length; const processedRows = []; mainLoop: for (let i = 0; i < this.parsedData.length; i++) { this.currentRow = i; this.progress = ((i + 1) / this.parsedData.length) * 100; // Simulate processing await this.sleep(100); // Process row here const row = this.parsedData[i]; const findUserResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?custnume=' + row[this.selectedColumns.kundennummer]); const findUserData = await findUserResponse.json(); if (findUserData.length === 0) { row.ont_sn = 'N/A - Kein Benutzer mit dieser Kundennummer gefunden'; processedRows.push(row); } else if (findUserData.length === 1) { const username = findUserData[0].username; const radacctResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + username); const radacctData = await radacctResponse.json(); row.ont_sn = radacctData.ont_sn || 'N/A - Keine ONT SN gefunden'; processedRows.push(row); } else if (findUserData.length > 1) { // check string simulairty of strasse, plz, stadt and atleast of 90% of each should be inside findUserData[].info // if not, ont_sn = N/A - Anschluss konnte nicht zugeordnet werden const strasse = row[this.selectedColumns.anschlussstrasse]; const plz = row[this.selectedColumns.anschlussplz]; const stadt = row[this.selectedColumns.anschlusscity]; const info = findUserData[0].info; for (let user of findUserData) { if (validateData(strasse, plz, stadt, info)) { const username = user.username; const radacctResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?skipAdditional=true&action2=fetchRadacct&username=' + username); const radacctData = await radacctResponse.json(); row.ont_sn = radacctData.ont_sn || 'N/A - Keine ONT SN gefunden'; processedRows.push(row); continue mainLoop; } } row.ont_sn = 'N/A - Anschluss konnte nicht zugeordnet werden'; processedRows.push(row); } } this.loading = false; this.processedData = processedRows; this.step = 3; }, downloadResults() { const ws = XLSX.utils.json_to_sheet(this.processedData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Results"); XLSX.writeFile(wb, "results.xlsx"); }, sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } }); Vue.component('radius-online-state', { props: ['username'], template: `
`, data: () => ({ data: null, observer: null, observed: false }), mounted() { this.observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting && !this.observed) { this.observed = true; this.fetchOnlineState(); } }, { threshold: 0.1 }); this.observer.observe(this.$refs.container); }, beforeDestroy() { this.observer?.disconnect(); }, methods: { async fetchOnlineState() { const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${this.username}`); if (response.ok) this.data = await response.json(); } } }) Vue.component('radius', { template: `
Kundennummer Username Info Status Aktionen
{{ user.customerNumber }} {{ user.username }} {{ user.info }}

Freie NAT Benutzer ({{ freeNatUsers.length }})

Username Info
{{ user.username }} {{ user.info }}

Freie STF Benutzer ({{ freeStfUsers.length }})

Username Info
{{ user.username }} {{ user.info }}
Status: {{ radacctData.online ? 'Online' : 'Offline' }}
IP: {{ radacctData.ip }}
Username: {{ radacctData.username }}
Customer Number: {{ radacctData.customerNumber }}
Customer Name: {{ radacctData.customerName }}
Info: {{ radacctData.info }}
WLAN Password: {{ radacctData.wlanPassword }}
Bandbreite: {{ radacctData.actualBandwidth }}
`, data() { return { view: 'radius', billAddrAutoCompleteUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1', radiusUsers: [], freeNatUsers: [], freeStfUsers: [], username: '', info: '', custnum: '', window: window, showRadacctModal: false, checkOnlineState: 0, radacctData: null, isLoading: false, searchCount: 0, } }, async mounted() { console.log("hallo"); await this.loadFreeUsers(); }, methods: { hideRadacctModal() { this.showRadacctModal = false; }, async loadRadiusUsers() { this.isLoading = true; this.radiusUsers = []; let custnum = ''; if (this.$refs.billAddr.displayValue.length > 5) { custnum = this.$refs.billAddr.displayValue.match(/\[(\d+)]/)[1]; } const params = new URLSearchParams({ username: this.username, info: this.info, custnum: custnum, }); const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?${params.toString()}`); if (response.ok) { const users = await response.json() if (users.length < 6) { this.checkOnlineState = 1; } this.radiusUsers = users; } else { console.error('Failed to load radius users'); } this.isLoading = false; this.searchCount = this.searchCount + 1; }, async fetchRadacctData(username) { const params = new URLSearchParams({ action2: 'fetchRadacct', username: username, }); const response = await fetch(`${window.TT_CONFIG['BASE_PATH']}/Radius/proxyUnsecureHTTPRequestToRadius?${params.toString()}`); if (response.ok) { this.radacctData = await response.json(); this.showRadacctModal = true; } else { console.error('Failed to fetch radacct data'); } }, async loadFreeUsers() { try { const natResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?action2=free_user&filter=nat'); const stfResponse = await fetch(window.TT_CONFIG['BASE_PATH'] + '/Radius/proxyUnsecureHTTPRequestToRadius?action2=free_user&filter=stf'); if (natResponse.ok && stfResponse.ok) { const natData = await natResponse.json(); const stfData = await stfResponse.json(); this.freeNatUsers = natData.users; this.freeStfUsers = stfData.users; } else { console.error('Failed to load free users'); } } catch (error) { console.error('Error loading free users:', error); } }, }, }); 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. Optional kann eine 'MAC' Spalte für eine alternative Suche verwendet werden.

{{ uploadError }}

Schritt 2: Ergebnisse

{{ 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, parsedData: [], processedData: [], originalHeaders: [], loading: false, progress: 0, currentRow: 0, totalRows: 0, currentSerial: '', uploadError: null, serialColumnName: 'Serial', macColumnName: 'MAC', fetchedKeys: { username: 'fetched_username', customerNumber: 'fetched_customerNumber', customerName: 'fetched_customerName', info: 'fetched_info' }, apiBasePath: window.TT_CONFIG?.BASE_PATH }; }, methods: { resetComponent() { Object.assign(this.$data, this.$options.data.call(this)); const input = this.$el.querySelector('input[type="file"]'); if (input) input.value = ''; }, async handleFileUpload(event) { const file = event.target.files[0]; this.uploadError = null; if (!file) return; this.loading = true; try { 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 workbook = XLSX.read(data, { type: 'array' }); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; this.parsedData = XLSX.utils.sheet_to_json(worksheet, { defval: "" }); if (!this.parsedData.length) { throw new Error("Die hochgeladene Datei ist leer oder konnte nicht gelesen werden."); } 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; } }, async loadXLSX() { 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); }); }, async startProcessing() { this.loading = true; this.totalRows = this.parsedData.length; this.processedData = []; this.progress = 0; this.currentRow = 0; 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=`; 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 newRow = { ...row }; const serialNumber = row[this.serialColumnName]?.trim(); this.currentSerial = `SN: ${serialNumber || 'Leer'}`; if (!serialNumber) { setRowStatus(newRow, 'Leere Seriennummer'); this.processedData.push(newRow); this.progress = ((i + 1) / this.totalRows) * 100; continue; } let found = false; 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(`Fetch error for SN ${serialNumber}:`, error); } 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; 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; this.currentSerial = ''; }, downloadResults() { if (!this.processedData.length) return; try { const dataToExport = this.processedData.map(row => { const exportRow = {}; 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]; 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"); const timestamp = new Date().toISOString().replace(/[-:.]/g, "").slice(0, 14); 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."); } }, sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, }, mounted() { if (!window.TT_CONFIG?.BASE_PATH) { console.warn(`Global TT_CONFIG.BASE_PATH not found. API calls will use fallback path: ${this.apiBasePath}`); } } });