Added new Radius ONT Parser to TheTool
This commit is contained in:
@@ -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")]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user