Add new components for managing free users and ONT lookup, enhance UI styling,...

This commit is contained in:
Luca Haid
2025-10-10 07:28:37 +00:00
parent 44b0eebb7c
commit 67e5a1c94c
8 changed files with 1409 additions and 863 deletions
+165
View File
@@ -0,0 +1,165 @@
/* ===== RadiusOntFinder.js =====
* Reverse lookup by ONT Serial (and optional MAC). Styling via shared ONT CSS utilities.
*/
Vue.component('radius-ont-finder', {
template: `
<div class="radius-scope ont-card">
<div v-if="step===1" class="block">
<div class="block-head">
<div class="h4"><i class="fa-duotone fa-file-spreadsheet"></i> Schritt 1 · Excel (XLSX) Upload</div>
<p class="muted small">
Datei muss die Spalte <code>Serial</code> enthalten. Optional <code>MAC</code> (12 Zeichen, ohne Doppelpunkte).
</p>
</div>
<label class="file-drop" @dragover.prevent @drop.prevent="onDrop">
<input type="file" accept=".xlsx" @change="handleFileUpload" hidden ref="fileInput">
<div class="file-cta">
<i class="fa-duotone fa-cloud-arrow-up"></i>
<div>Hierhin ziehen oder <button type="button" class="link-btn" @click="$refs.fileInput.click()">Datei auswählen</button></div>
</div>
</label>
<div v-if="uploadError" class="alert error mt-2">{{ uploadError }}</div>
</div>
<div v-if="step===2" class="block">
<div class="block-head">
<div class="h4"><i class="fa-duotone fa-list-check"></i> Ergebnisse</div>
<div class="cluster">
<button class="primary-btn" @click="downloadResults"><i class="fa-duotone fa-download"></i> Ergebnisse herunterladen</button>
<button class="ghost-btn" @click="resetComponent"><i class="fa-duotone fa-rotate-right"></i> Neue Datei</button>
</div>
</div>
<div class="table-wrap">
<table class="tt-table compact">
<thead>
<tr>
<th v-for="h in originalHeaders" :key="'h'+h">{{ h }}</th>
<th>Username</th>
<th>Kundennummer</th>
<th>Kundenname</th>
<th>Info</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in processedData" :key="i" class="row-fade-in">
<td v-for="h in originalHeaders" :key="h+i">{{ row[h] }}</td>
<td class="mono">{{ row.fetched_username }}</td>
<td class="mono">{{ row.fetched_customerNumber }}</td>
<td class="clamp-2">{{ row.fetched_customerName }}</td>
<td class="clamp-2 mono">{{ row.fetched_info }}</td>
</tr>
<tr v-if="processedData.length===0">
<td :colspan="originalHeaders.length + 4" class="muted center p-lg">Keine Daten</td>
</tr>
</tbody>
</table>
</div>
</div>
<transition name="fade">
<div v-if="loading" class="overlay">
<div class="ont-loading-card pop">
<div class="h5">Verarbeitung läuft...</div>
<p class="muted small">Aktuell: {{ currentSerial || '—' }}</p>
<div class="progress-bar is-yellow mt-3"><div class="bar" :style="{width: progress + '%'}"></div></div>
<div class="muted small mt-2">Verarbeite Zeile {{ currentRow + 1 }} von {{ totalRows }}</div>
</div>
</div>
</transition>
</div>
`,
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 i=this.$el.querySelector('input[type="file"]'); if (i) i.value=''; },
onDrop(e){ const f=e.dataTransfer.files?.[0]; if (f) this.readXlsx(f); },
async handleFileUpload(e){ const f=e.target.files?.[0]; if (f) this.readXlsx(f); },
async readXlsx(file){
this.uploadError=null; this.loading=true;
try{
await this.loadXLSX();
const arr = await new Promise((resolve,reject)=>{ const r=new FileReader(); r.onload=ev=>resolve(new Uint8Array(ev.target.result)); r.onerror=()=>reject(new Error('Fehler beim Lesen.')); r.readAsArrayBuffer(file); });
const wb = XLSX.read(arr, { type:'array' });
const ws = wb.Sheets[wb.SheetNames[0]];
this.parsedData = XLSX.utils.sheet_to_json(ws, { defval:'' });
if (!this.parsedData.length) throw new Error('Die Datei ist leer.');
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(e){ this.uploadError=e.message; this.loading=false; this.step=1; }
},
async loadXLSX(){
if (window.XLSX) return;
await new Promise((res,rej)=>{ const s=document.createElement('script'); s.src='https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'; s.onload=res; s.onerror=()=>rej(new Error('XLSX konnte nicht geladen werden.')); document.head.appendChild(s); });
},
async startProcessing(){
this.loading=true; this.totalRows=this.parsedData.length; this.processedData=[]; this.progress=0; this.currentRow=0;
const snApi = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?ont_sn=`;
const userApi= `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?username=`;
const sesApi = `${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?action2=find_by_current_session&mac=`;
const setRow = (row, msg, data={})=>{
const d={ username:`N/A - ${msg}`, customerNumber:'N/A', customerName:'N/A', info:'N/A' };
Object.keys(this.fetchedKeys).forEach(k => row[this.fetchedKeys[k]] = data[k] || d[k]);
};
for (const [i,row] of this.parsedData.entries()){
this.currentRow=i; const out={...row};
const sn=(''+(row[this.serialColumnName]||'')).trim(); this.currentSerial = `SN: ${sn || '—'}`;
let found=false;
if (sn){
try{ const r=await fetch(snApi+encodeURIComponent(sn)); if (r.ok){ const j=await r.json(); if (Array.isArray(j) && j.length>0){ setRow(out,'', j[0]); found=true; } } } catch {}
}
if (!found && this.originalHeaders.includes(this.macColumnName)){
const macRaw=(''+(row[this.macColumnName]||'')).trim();
if (macRaw && macRaw.length===12){
const mac = macRaw.toUpperCase().match(/.{1,2}/g).join(':');
try{
const s=await fetch(sesApi+encodeURIComponent(mac));
if (s.ok){ const ses=await s.json(); if (Array.isArray(ses) && ses.length>0){
const uname = ses[0];
const u=await fetch(`${userApi}${encodeURIComponent(uname)}&info=&custnum=`);
if (u.ok){ const d=await u.json(); if (Array.isArray(d) && d.length>0){ setRow(out,'', d[0]); found=true; } }
}}
} catch {}
}
}
if (!found) setRow(out, 'Keinen Benutzer gefunden');
this.processedData.push(out);
this.progress=((i+1)/this.totalRows)*100;
if ((i+1)%20===0) await new Promise(r=>setTimeout(r,20));
}
this.loading=false; this.step=2; this.currentSerial='';
},
downloadResults(){
if (!this.processedData.length) return;
try{
const data = this.processedData.map(r=>{
const o={}; this.originalHeaders.forEach(h=>o[h]=r[h]);
o['Username']=r[this.fetchedKeys.username];
o['Kundennummer']=r[this.fetchedKeys.customerNumber];
o['Kundenname']=r[this.fetchedKeys.customerName];
o['Info']=r[this.fetchedKeys.info];
return o;
});
const ws=XLSX.utils.json_to_sheet(data); const wb=XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'ONT_Finder_Results');
const ts=new Date().toISOString().replace(/[-:.]/g,'').slice(0,14);
XLSX.writeFile(wb, `ont_finder_results_${ts}.xlsx`);
}catch{ if(window.notify) window.notify('error', 'Fehler beim Erstellen der Excel-Datei.'); }
}
}
});