Radius/add network structure
This commit is contained in:
@@ -1,29 +1,203 @@
|
||||
/* ===== RadiusOntFinder.js ===== */
|
||||
Vue.component('radius-ont-finder', {
|
||||
/* ===== RadiusOntFinder.js (Vue 3 + TT-Core) ===== */
|
||||
|
||||
const RadiusOntFinder = {
|
||||
name: 'RadiusOntFinder',
|
||||
template: `
|
||||
<div class="radius-scope ont-card">
|
||||
<div class="tt-scope ont-card">
|
||||
<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 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>
|
||||
<tt-file-dropzone accept=".xlsx,.xls" @file-selected="readXlsx" />
|
||||
<div v-if="uploadError" class="alert error mt-2">{{ uploadError }}</div>
|
||||
</div>
|
||||
<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="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>
|
||||
<tt-loading-indicator
|
||||
v-if="loading"
|
||||
:text="currentSerial"
|
||||
:progress="progress"
|
||||
style="min-height: 200px;"
|
||||
/>
|
||||
<tt-data-table
|
||||
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>
|
||||
</tt-data-table>
|
||||
<div v-if="!loading && processedData.length" class="results-summary">
|
||||
{{ processedData.length }} Zeilen verarbeitet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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 }),
|
||||
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=''; },
|
||||
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.'); } }
|
||||
resetComponent() {
|
||||
Object.assign(this.$data, this.$options.data.call(this));
|
||||
const i = this.$el.querySelector('input[type="file"]');
|
||||
if (i) i.value = '';
|
||||
},
|
||||
async readXlsx(file) {
|
||||
this.uploadError = null;
|
||||
try {
|
||||
await window.TT_CORE.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 { data } = await axios.get(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius`, {
|
||||
params: { ont_sn: sn }
|
||||
});
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setRow(out, '', data[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 { data: ses } = await axios.get(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius`, {
|
||||
params: { action2: 'find_by_current_session', mac }
|
||||
});
|
||||
if (Array.isArray(ses) && ses.length > 0) {
|
||||
const { data: d } = await axios.get(`${this.apiBasePath}/Radius/proxyUnsecureHTTPRequestToRadius`, {
|
||||
params: { username: ses[0], info: '', custnum: '' }
|
||||
});
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Register component with Vue 3 app
|
||||
if (window.VueApp) {
|
||||
window.VueApp.component('radius-ont-finder', RadiusOntFinder);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user