Added new Radius ONT Parser to TheTool

This commit is contained in:
Luca Haid
2025-01-29 16:09:44 +01:00
parent b96d9afdef
commit f4fb3eb39d
3 changed files with 311 additions and 15 deletions

View File

@@ -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")]);
}

View File

@@ -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;
}

View File

@@ -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: `
<div class="container mt-4">
<!-- Step 1: File Upload -->
<div v-if="step === 1" class="card">
<div class="card-body">
<h4 class="card-title">Schritt 1: Excel (XLSX) Upload</h4>
<input type="file" class="form-control" @change="handleFileUpload" accept=".xlsx">
</div>
</div>
<!-- Step 2: Column Mapping -->
<div v-if="step === 2" class="card mt-4">
<div class="card-body">
<h4 class="card-title">Schritt 2: Spaltenzuordnung</h4>
<div class="row">
<div class="col-md-6" v-for="(field, index) in requiredFields" :key="index">
<div class="form-group">
<label>{{ field.label }}</label>
<select class="form-control" v-model="selectedColumns[field.key]">
<option v-for="header in headers" :value="header">{{ header }}</option>
</select>
</div>
</div>
</div>
<button class="btn btn-primary mt-3" @click="startProcessing">Start Processing</button>
</div>
</div>
<!-- Step 3: Result Download -->
<div v-if="step === 3" class="card mt-4">
<div class="card-body">
<h4 class="card-title">Schritt 3: Ergebnisse</h4>
<tt-button icon="fas fa-download" text="Neue Excel herunterladen" additional-class="btn-primary" @click="downloadResults"/>
<table class="table mt-4">
<thead>
<tr>
<template v-for="header in requiredFields">
<th>{{ header.label }}</th>
</template>
<th>ONT SN</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in processedData" :key="index">
<td>{{ row[selectedColumns.kundennummer] }}</td>
<td>{{ row[selectedColumns.anschlussstrasse] }}</td>
<td>{{ row[selectedColumns.anschlussplz] }}</td>
<td>{{ row[selectedColumns.anschlusscity] }}</td>
<td>{{ row.ont_sn }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Loading Screen -->
<div v-if="loading" class="loading-overlay mt-4">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
:style="{ width: progress + '%' }">
{{ Math.round(progress) }}%
</div>
</div>
<div class="text-center mt-2">Processing {{ currentRow + 1 }} of {{ totalRows }}</div>
</div>
</div>
`,
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: `
<tt-card>
@@ -6,18 +282,9 @@ Vue.component('radius', {
</template>
<div class="radius-view-selector">
<tt-button
icon="fas fa-user"
additional-class="btn-primary"
text="Radius Benutzer"
@click="view = 'radius'"
></tt-button>
<tt-button
icon="fas fa-user-plus"
additional-class="btn-primary"
text="Freie Radius Benutzer"
@click="view = 'free'"
></tt-button>
<tt-button icon="fas fa-user" additional-class="btn-primary" text="Radius Benutzer" @click="view = 'radius'"/>
<tt-button icon="fas fa-user-plus" additional-class="btn-primary" text="Freie Radius Benutzer" @click="view = 'free'"/>
<tt-button text="Radius ONT Parser" icon="fas fa-cogs" additional-class="btn-primary" @click="view = 'ont'" v-show="window['TT_CONFIG']['CAN_BILLING'] === '1'"/>
</div>
<div v-if="view === 'radius'">
@@ -94,6 +361,10 @@ Vue.component('radius', {
</table>
</div>
</div>
<div v-if="view === 'ont'">
<radius-ont-parser/>
</div>
<tt-modal :show.sync="showRadacctModal" title="Radacct Data" :save="false" :delete="false">
@@ -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({