diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 782d60660..7e129785d 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -15,7 +15,7 @@ class RadiusController extends mfBaseController { protected function indexAction() { $this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]); - Helper::renderVue($this, $this->mod, "Radius", []); + Helper::renderVue($this, $this->mod, "Radius", ['CAN_BILLING' => $this->me->can("Billing")]); } diff --git a/public/js/pages/Radius/Radius.css b/public/js/pages/Radius/Radius.css index d219763fd..66f1a5ee1 100644 --- a/public/js/pages/Radius/Radius.css +++ b/public/js/pages/Radius/Radius.css @@ -6,6 +6,11 @@ margin-bottom: 20px; } +.radius-view-selector > *, +.radius-view-selector > * > * { + width: 100%; +} + @media (max-width: 576px) { .radius-view-selector { grid-template-columns: 1fr; @@ -53,3 +58,24 @@ padding: 15px; border-radius: 5px; } + + + +/* +RADIUS ONT PARSER +*/ +.loading-overlay { + position: relative; + padding: 20px; + background: white; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.progress { + height: 30px; +} + +.progress-bar { + transition: width 0.3s ease; +} \ No newline at end of file diff --git a/public/js/pages/Radius/Radius.js b/public/js/pages/Radius/Radius.js index dd5d56ec9..182f7a37b 100644 --- a/public/js/pages/Radius/Radius.js +++ b/public/js/pages/Radius/Radius.js @@ -1,3 +1,279 @@ +// 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', { template: ` @@ -6,18 +282,9 @@ Vue.component('radius', {
- - + + +
@@ -94,6 +361,10 @@ Vue.component('radius', {
+ +
+ +
@@ -156,13 +427,12 @@ Vue.component('radius', { }, methods: { hideRadacctModal() { - console.log('hideRadacctModal'); this.showRadacctModal = false; }, async loadRadiusUsers() { let custnum = ''; if (this.$refs.billAddr.displayValue.length > 5) { - custnum = this.$refs.billAddr.displayValue.match(/\[(\d+)\]/)[1]; + custnum = this.$refs.billAddr.displayValue.match(/\[(\d+)]/)[1]; } const params = new URLSearchParams({