Add new components for managing free users and ONT lookup, enhance UI styling,...
This commit is contained in:
@@ -1,165 +1,29 @@
|
||||
/* ===== RadiusOntFinder.js =====
|
||||
* Reverse lookup by ONT Serial (and optional MAC). Styling via shared ONT CSS utilities.
|
||||
*/
|
||||
|
||||
/* ===== RadiusOntFinder.js ===== */
|
||||
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 v-if="step===1">
|
||||
<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>.</p></div>
|
||||
<radius-file-drop @file-selected="readXlsx" /><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 v-if="step===2">
|
||||
<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" :disabled="loading"><i class="fa-duotone fa-download"></i> Ergebnisse herunterladen</button><button class="ghost-btn" @click="resetComponent" :disabled="loading"><i class="fa-duotone fa-rotate-right"></i> Neue Datei</button></div></div>
|
||||
<div class="results-container mt-between">
|
||||
<radius-processing-indicator v-if="loading" :progress="progress" :current-row="currentRow" :total-rows="totalRows" :current-serial="currentSerial" />
|
||||
<radius-table-view v-else :items="processedData" :has-searched="true" no-results-placeholder-text="Keine Daten verarbeitet.">
|
||||
<template #head><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></template>
|
||||
<template #row="{ item }"><td v-for="h in originalHeaders" :key="h+item.Serial">{{ item[h] }}</td><td class="mono">{{ item.fetched_username }}</td><td class="mono">{{ item.fetched_customerNumber }}</td><td class="clamp-2">{{ item.fetched_customerName }}</td><td class="clamp-2 mono">{{ item.fetched_info }}</td></template>
|
||||
</radius-table-view>
|
||||
<div v-if="!loading && processedData.length" class="results-summary">{{ processedData.length }} Zeilen verarbeitet</div>
|
||||
</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
|
||||
};
|
||||
},
|
||||
data: () => ({ 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.'); }
|
||||
}
|
||||
async readXlsx(file){ this.uploadError=null; try{ await window.RadiusUtils.loadScript('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.0/xlsx.full.min.js'); const arr = await new Promise((res,rej)=>{ const r=new FileReader(); r.onload=e=>res(new Uint8Array(e.target.result)); r.onerror=()=>rej(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.step=1; } },
|
||||
async startProcessing(){ this.step = 2; this.loading = true; this.totalRows=this.parsedData.length; this.processedData=[]; 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(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?ont_sn=${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(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?action2=find_by_current_session&mac=${encodeURIComponent(mac)}`); if(s.ok){ const ses=await s.json(); if(Array.isArray(ses)&&ses.length>0){ const u=await fetch(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius?username=${encodeURIComponent(ses[0])}&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.currentSerial=''; },
|
||||
downloadResults(){ if (!this.processedData.length) return; try{ const data=this.processedData.map(r=>{ const o={}; this.originalHeaders.forEach(h=>o[h]=r[h]); Object.keys(this.fetchedKeys).forEach(k=>{const K=k.charAt(0).toUpperCase()+k.slice(1).replace('Number','nummer').replace('Name','name'); o[K]=r[this.fetchedKeys[k]];}); 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'); XLSX.writeFile(wb, `ont_finder_results_${new Date().toISOString().replace(/[-:.]/g,'').slice(0,14)}.xlsx`); } catch { if(window.notify) window.notify('error', 'Fehler beim Erstellen der Excel-Datei.'); } }
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user