827 lines
42 KiB
JavaScript
827 lines
42 KiB
JavaScript
Vue.component('radius-users', {
|
||
template: `
|
||
<div class="radius-scope">
|
||
<div class="filters-layout">
|
||
<div class="field">
|
||
<radius-autocomplete v-model="billAddrDisplay" :wide="true" placeholder="Kunde suchen"
|
||
@select="onAddrSelect" @enter="loadRadiusUsers" @mode-change="onModeChange"/>
|
||
</div>
|
||
<div class="field">
|
||
<div class="input-wrap ip-field-wrapper"><span class="ip-focus-tooltip">z.B. nat* für lazy Suche</span><i
|
||
class="fa-duotone fa-user input-icon"></i><input class="ri" v-model="username" placeholder="Username"
|
||
@keydown.enter="loadRadiusUsers" autocomplete="off"
|
||
autocapitalize="none" autocorrect="off"
|
||
inputmode="text" name="radius-username"/></div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="input-wrap ip-field-wrapper"><span class="ip-focus-tooltip">Prefixe: '=' exakt, '*' Verlauf (lazy), '*=' Verlauf (exakt)</span>
|
||
<div class="input-wrap"><i class="fa-duotone fa-network-wired input-icon"></i><input class="ri"
|
||
v-model="ip"
|
||
placeholder="IP-Adresse"
|
||
@keydown.enter="loadRadiusUsers"
|
||
autocomplete="off"
|
||
autocapitalize="none"
|
||
autocorrect="off"
|
||
inputmode="decimal"
|
||
name="radius-ip"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="input-wrap"><i class="fa-duotone fa-note-sticky input-icon"></i><input class="ri" v-model="info"
|
||
placeholder="Info"
|
||
@keydown.enter="loadRadiusUsers"/>
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="gap: 8px;">
|
||
<div class="field"><label class="switch-field"><span class="mini muted">Online-Status</span><span
|
||
class="switch"><input type="checkbox" v-model="checkOnlineState"><span class="switch-track"><i
|
||
class="fa-duotone fa-signal-bars-good on"></i><i class="fa-duotone fa-signal-bars-slash off"></i></span></span></label>
|
||
</div>
|
||
<button class="primary-btn" @click="loadRadiusUsers" :disabled="isLoading" style="flex-grow: 1;"><span
|
||
v-if="!isLoading"><i class="fa-duotone fa-magnifying-glass"></i></span><span v-else
|
||
class="btn-loader"></span>
|
||
</button>
|
||
<button class="danger-btn" @click="clearFilters" :disabled="!hasFilters" data-tooltip="Eingaben leeren"
|
||
data-tooltip-align="left"><i class="fa-duotone fa-xmark"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="results-container mt-between">
|
||
<radius-table-view :items="visibleUsers" :is-loading="isLoading" :has-searched="hasSearched"
|
||
:skeleton-row-count="6"
|
||
initial-placeholder-text="Beginnen Sie Ihre Suche, indem Sie Filter eingeben.">
|
||
<template #head>
|
||
<thead>
|
||
<tr>
|
||
<th style="text-align: center; width: 170px;">Kundennummer</th>
|
||
<th style="text-align: center; width: 183px;">Username</th>
|
||
<th style="text-align: center">Info</th>
|
||
<th style="text-align: center; width: 190px;">Status</th>
|
||
<th style="text-align: center; width: 115px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
</template>
|
||
<template #skeleton-row>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line" style="height: 36px;"></div>
|
||
</td>
|
||
</template>
|
||
<template #row="{ item }">
|
||
<td><a class="link" target="_blank"
|
||
:href="window.TT_CONFIG.BASE_PATH + '/Address?filter%5Bcustomer_number%5D=' + item.customerNumber"
|
||
data-tooltip="Kunden in neuem Tab öffnen" data-tooltip-align="right">{{ item.customerNumber }}</a>
|
||
</td>
|
||
<td class="nowrap"><a class="link" target="_blank"
|
||
:href="'http://radius.xinon.at/edit_user.php?user=' + item.username"
|
||
data-tooltip="User in Radius öffnen"
|
||
data-tooltip-align="right">{{ item.username }}</a>
|
||
<button class="icon-btn sm" data-tooltip="Kopieren" data-tooltip-align="right"
|
||
@click="copy(item.username, $event)"><i class="fa-duotone fa-copy copy-icon"></i><i
|
||
class="fa-duotone fa-check check-icon"></i></button>
|
||
</td>
|
||
<td class="mono clamp-2">{{ item.info || '—' }}</td>
|
||
<td>
|
||
<radius-online-state
|
||
v-if="checkOnlineState"
|
||
:username="item.username"
|
||
:key="item.username + '_'+searchCount"
|
||
@scan-ip="onScanIp($event, item)"
|
||
/>
|
||
</td>
|
||
<td class="nowrap cluster" style="gap: 4px; justify-content: center;">
|
||
<button class="ghost-btn" @click="fetchRadacctData(item.username)" data-tooltip="Details"><i
|
||
class="fa-duotone fa-circle-info"></i></button>
|
||
<button class="ghost-btn" @click="openTransferModal(item.username)" data-tooltip="Transfer Statistik"
|
||
data-tooltip-align="left"><i class="fa-duotone fa-chart-line"></i></button>
|
||
</td>
|
||
</template>
|
||
<template #observer>
|
||
<div ref="sentinel" style="height: 1px;"></div>
|
||
</template>
|
||
</radius-table-view>
|
||
<div v-if="hasSearched" class="results-summary"><span v-if="isLoading">Suche läuft...</span><span
|
||
v-else-if="radiusUsers.length">{{ radiusUsers.length }} Treffer gefunden</span></div>
|
||
</div>
|
||
<radius-modal :show="showRadacctModal" title="RADIUS Daten" @close="showRadacctModal=false">
|
||
<div class="kv-redesign">
|
||
<div class="kv-row"><span class="kv-label">Status</span>
|
||
<div class="kv-value">
|
||
<div v-if="radacctData"><strong class="chip"
|
||
:class="radacctData.online ? 'ok' : 'bad'">{{ radacctData.online ? 'Online' : 'Offline' }}</strong>
|
||
</div>
|
||
<div v-else>
|
||
<div class="skeleton-line" style="width: 80px; --h: 24px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kv-row"><span class="kv-label">IP</span>
|
||
<div class="kv-value">
|
||
<div v-if="radacctData" class="inline-copy"><code v-if="radacctData.ip">{{ radacctData.ip }}</code><code
|
||
v-else>—</code>
|
||
<button v-if="radacctData.ip" class="icon-btn sm" @click="copy(radacctData.ip, $event)"
|
||
data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i
|
||
class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
<div v-else>
|
||
<div class="skeleton-line" style="width: 120px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kv-row"><span class="kv-label">Username</span>
|
||
<div class="kv-value">
|
||
<div v-if="radacctData" class="inline-copy"><a class="link" target="_blank"
|
||
:href="'http://radius.xinon.at/edit_user.php?user=' + radacctData.username"
|
||
data-tooltip="User in Radius öffnen">{{ radacctData.username }}</a>
|
||
<button class="icon-btn sm" @click="copy(radacctData.username, $event)" data-tooltip="Kopieren"><i
|
||
class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
<div v-else>
|
||
<div class="skeleton-line" style="width: 150px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<template v-if="radacctData">
|
||
<div class="kv-row"><span class="kv-label">Kundennummer</span><code
|
||
class="kv-value">{{ radacctData.customerNumber || '—' }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Kundenname</span>
|
||
<div class="kv-value clamp-2">{{ radacctData.customerName || '—' }}</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Info</span>
|
||
<div class="kv-value clamp-3 mono small">{{ radacctData.info || '—' }}</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">WLAN Password</span><code
|
||
class="kv-value mono">{{ radacctData.wlanPassword || '—' }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Bandbreite</span><code
|
||
class="kv-value">{{ radacctData.actualBandwidth || '—' }}</code></div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="kv-row"><span class="kv-label">Kundennummer</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 70px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Kundenname</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 200px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Info</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="--h:14px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">WLAN Password</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 100px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Bandbreite</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 180px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</radius-modal>
|
||
<radius-modal :show="showTransferModal" :title="'Transfer Statistik für ' + transferModalUsername"
|
||
@close="closeTransferModal" modal-class="modal-card-wide">
|
||
<div class="modal-body-scrollable">
|
||
<div v-if="transferYearlyData || transferInitialLoading">
|
||
<div class="unselectable">
|
||
<div class="cluster"
|
||
style="justify-content: space-between; margin-bottom: 16px; flex-wrap: nowrap; margin-top: 4px; padding-left: 8px;">
|
||
<div class="cluster">
|
||
<div class="custom-dropdown">
|
||
<button class="dropdown-toggle"
|
||
@click="!transferInitialLoading && (showYearDropdown = !showYearDropdown)"
|
||
:class="{'is-open': showYearDropdown}"><span>{{ transferYear }}</span><i
|
||
class="fa-solid fa-chevron-down"></i></button>
|
||
<transition name="ac-pop">
|
||
<div v-if="showYearDropdown" class="dropdown-panel">
|
||
<div v-for="y in availableYears" :key="y" class="dropdown-item" @click="selectYear(y)">
|
||
{{ y }}
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
<div class="cluster" style="gap: 4px;">
|
||
<button v-for="m in allMonths" :key="m.month" class="tab-btn"
|
||
:class="{active: transferMonth === m.month}" :disabled="isMonthDisabled(m.month)"
|
||
@click="changeTransferMonth(m.month)">{{ m.name }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="gap: 16px;">
|
||
<div class="muted small mono" style="text-align: right; flex-shrink: 0;">Gesamt {{ transferYear }}
|
||
:<br><strong v-if="transferInitialLoading || !transferYearlyData">
|
||
<div class="skeleton-line" style="width: 110px; height: 16px; margin-left:auto;"></div>
|
||
</strong><strong
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferYearlyData.yearlySummary.grandTotalBytes) }}</strong>
|
||
</div>
|
||
<button class="ghost-btn" @click="prepareEmailModal"
|
||
:disabled="transferInitialLoading || !transferYearlyData || !transferYearlyData.yearlySummary || transferYearlyData.yearlySummary.grandTotalBytes === 0"
|
||
data-tooltip="Statistik per E-Mail senden" data-tooltip-align="bottom-left"
|
||
data-tooltip-wrap="true"><i class="fa-duotone fa-paper-plane"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="grid g-4 cols-4">
|
||
<div class="stat-card-v2 stat-total">
|
||
<div class="stat-icon"><i class="fa-duotone fa-grid-2"></i></div>
|
||
<div>
|
||
<div class="stat-label">Monat gesamt</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 100px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.grandTotalBytes || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2 stat-download">
|
||
<div class="stat-icon"><i class="fa-duotone fa-arrow-down-to-line"></i></div>
|
||
<div>
|
||
<div class="stat-label">Download</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 100px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalDownloadBytes || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2 stat-upload">
|
||
<div class="stat-icon"><i class="fa-duotone fa-arrow-up-from-line"></i></div>
|
||
<div>
|
||
<div class="stat-label">Upload</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 100px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalUploadBytes || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2 stat-duration">
|
||
<div class="stat-icon"><i class="fa-duotone fa-hourglass-clock"></i></div>
|
||
<div>
|
||
<div class="stat-label">Dauer</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 80px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatDuration(transferMonthlyData?.summary?.totalDurationSeconds || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card mt-3" style="height: 250px;">
|
||
<div v-if="transferMonthlyLoading || transferInitialLoading" class="chart-placeholder">
|
||
<div class="skeleton-line" style="width: 100%; height: 100%; border-radius: var(--radius);"></div>
|
||
</div>
|
||
<div
|
||
v-else-if="!transferMonthlyData || !transferMonthlyData.details || !transferMonthlyData.details.length"
|
||
class="chart-placeholder"><i class="fa-duotone fa-chart-pie"></i><span>Keine Daten in diesem Monat verfügbar</span>
|
||
</div>
|
||
<canvas
|
||
v-show="!transferMonthlyLoading && !transferInitialLoading && transferMonthlyData?.details?.length"
|
||
ref="transferChartCanvas"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap mt-3" style="height: 350px;">
|
||
<div
|
||
v-if="!transferInitialLoading && !transferMonthlyLoading && (!transferMonthlyData || !transferMonthlyData.details || !transferMonthlyData.details.length)"
|
||
class="table-placeholder-fixed-height"><i class="fa-duotone fa-database"></i><span>Keine detaillierten Daten für diesen Monat.</span>
|
||
</div>
|
||
<table v-else class="tt-table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>Startzeit</th>
|
||
<th>Dauer</th>
|
||
<th>IP-Adresse</th>
|
||
<th style="text-align: right;">Download</th>
|
||
<th style="text-align: right;">Upload</th>
|
||
<th style="text-align: right;">Gesamt</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-if="transferInitialLoading || transferMonthlyLoading">
|
||
<tr v-for="n in 10" :key="'skel'+n">
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
<template v-else>
|
||
<tr v-for="(d, i) in transferMonthlyData.details" :key="i">
|
||
<td class="mono small">{{ d.startTime }}</td>
|
||
<td class="mono small">{{ window.RadiusUtils.formatDuration(d.durationSeconds) }}</td>
|
||
<td class="mono small">{{ d.ipAddress }}</td>
|
||
<td class="mono small" style="text-align: right;">
|
||
{{ window.RadiusUtils.formatBytes(d.downloadBytes) }}
|
||
</td>
|
||
<td class="mono small" style="text-align: right;">
|
||
{{ window.RadiusUtils.formatBytes(d.uploadBytes) }}
|
||
</td>
|
||
<td class="mono small" style="text-align: right;">
|
||
<strong>{{ window.RadiusUtils.formatBytes(d.totalBytes) }}</strong></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="!transferInitialLoading" class="table-placeholder" style="min-height: 400px;"><i
|
||
class="fa-duotone fa-wifi-slash"></i>
|
||
<div>Daten konnten nicht geladen werden.</div>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
<radius-modal :show="showEmailModal" title="Statistik per E-Mail senden" @close="showEmailModal=false">
|
||
<div>
|
||
<div class="field"><label style="margin-bottom: 8px; font-size: 14px;">Empfänger-E-Mail</label>
|
||
<div class="input-wrap"><i class="fa-duotone fa-envelope input-icon"></i><input class="ri" type="email"
|
||
v-model.trim="recipientEmail"
|
||
placeholder="name@domain.com"
|
||
@keydown.enter="isValidEmail && !isSendingEmail && sendTransferEmail()"
|
||
autocomplete="nope"
|
||
data-1p-ignore
|
||
data-lpignore="true"
|
||
data-form-type="other"
|
||
data-bwignore></div>
|
||
<p v-if="recipientEmail && !isValidEmail" class="muted small" style="color: var(--bad); margin-top: 4px;">
|
||
Bitte geben Sie eine gültige E-Mail-Adresse ein.</p></div>
|
||
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
|
||
<button class="ghost-btn" @click="showEmailModal=false" :disabled="isSendingEmail">Abbrechen</button>
|
||
<button class="primary-btn" @click="sendTransferEmail" :disabled="!isValidEmail || isSendingEmail"
|
||
style="min-width: 100px;"><span v-if="!isSendingEmail">Senden</span><span v-else
|
||
class="btn-loader"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
<radius-modal :show="showExtensionIdModal" title="Extension ID Konfigurieren" @close="showExtensionIdModal=false">
|
||
<div>
|
||
<div class="field"><label style="margin-bottom: 8px; font-size: 14px;">Chrome Extension ID</label>
|
||
<div class="input-wrap"><i class="fa-duotone fa-puzzle-piece input-icon"></i><input class="ri" type="text"
|
||
v-model.trim="extensionId"
|
||
placeholder="z.B. jglijfiddilckddlmbnlojmmlahboffh">
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
|
||
<button class="primary-btn" @click="saveExtensionId">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
</div>
|
||
`,
|
||
data: () => ({
|
||
window: window,
|
||
billAddrDisplay: '',
|
||
billAddrCustnum: '',
|
||
username: '',
|
||
ip: '',
|
||
info: '',
|
||
searchMode: 'autocomplete',
|
||
radiusUsers: [],
|
||
checkOnlineState: false,
|
||
isLoading: false,
|
||
showRadacctModal: false,
|
||
radacctData: null,
|
||
searchCount: 0,
|
||
hasSearched: false,
|
||
visibleCount: 50,
|
||
observer: null,
|
||
showTransferModal: false,
|
||
transferInitialLoading: false,
|
||
transferMonthlyLoading: false,
|
||
transferModalUsername: '',
|
||
transferYear: new Date().getFullYear(),
|
||
transferMonth: new Date().getMonth() + 1,
|
||
transferYearlyData: null,
|
||
transferMonthlyData: null,
|
||
transferChartInstance: null,
|
||
showYearDropdown: false,
|
||
isSendingEmail: false,
|
||
showEmailModal: false,
|
||
recipientEmail: '',
|
||
showExtensionIdModal: false,
|
||
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh'
|
||
}),
|
||
computed: {
|
||
hasFilters() {
|
||
return this.billAddrDisplay || this.username || this.ip || this.info;
|
||
},
|
||
visibleUsers() {
|
||
return this.radiusUsers.slice(0, this.visibleCount);
|
||
},
|
||
availableYears() {
|
||
const c = new Date().getFullYear(), s = 2021;
|
||
if (s > c) return [c];
|
||
return Array.from({length: c - s + 1}, (_, i) => c - i);
|
||
},
|
||
allMonths() {
|
||
return Array.from({length: 12}, (_, i) => ({
|
||
month: i + 1,
|
||
name: new Date(2000, i, 1).toLocaleString('de-DE', {month: 'short'})
|
||
}));
|
||
},
|
||
isValidEmail() {
|
||
if (!this.recipientEmail) return false;
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.recipientEmail);
|
||
}
|
||
},
|
||
mounted() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const infoParam = urlParams.get('info');
|
||
if (infoParam) {
|
||
this.info = infoParam;
|
||
this.loadRadiusUsers();
|
||
}
|
||
this.observer = new IntersectionObserver(([e]) => {
|
||
if (e && e.isIntersecting) this.loadMore();
|
||
}, {root: this.$refs.tableWrap, threshold: 0.1});
|
||
if (this.$refs.sentinel) this.observer.observe(this.$refs.sentinel);
|
||
|
||
const savedExtensionId = localStorage.getItem('radiusExtensionId');
|
||
if (savedExtensionId) {
|
||
this.extensionId = savedExtensionId;
|
||
}
|
||
window.addEventListener('keydown', this.handleKeydown);
|
||
},
|
||
beforeDestroy() {
|
||
if (this.observer) this.observer.disconnect();
|
||
if (this.transferChartInstance) this.transferChartInstance.destroy();
|
||
window.removeEventListener('keydown', this.handleKeydown);
|
||
},
|
||
updated() {
|
||
if (this.observer && this.$refs.sentinel) {
|
||
this.observer.disconnect();
|
||
this.observer.observe(this.$refs.sentinel);
|
||
}
|
||
},
|
||
methods: {
|
||
handleKeydown(e) {
|
||
console.log(e);
|
||
if (e.code === 'KeyE' && e.ctrlKey && e.altKey) {
|
||
e.preventDefault();
|
||
this.openExtensionIdModal();
|
||
}
|
||
},
|
||
openExtensionIdModal() {
|
||
this.showExtensionIdModal = true;
|
||
},
|
||
saveExtensionId() {
|
||
localStorage.setItem('radiusExtensionId', this.extensionId);
|
||
this.showExtensionIdModal = false;
|
||
window.notify('success', 'Extension ID gespeichert.');
|
||
},
|
||
onAddrSelect({custnum, display}) {
|
||
this.billAddrCustnum = custnum || '';
|
||
this.billAddrDisplay = display || '';
|
||
},
|
||
onModeChange(newMode) {
|
||
this.searchMode = newMode;
|
||
},
|
||
async loadRadiusUsers() {
|
||
this.isLoading = true;
|
||
this.radiusUsers = [];
|
||
this.hasSearched = true;
|
||
this.visibleCount = 50;
|
||
try {
|
||
const p = new URLSearchParams({
|
||
username: this.username || '',
|
||
info: this.info || '',
|
||
ip: this.ip || ''
|
||
});
|
||
if (this.searchMode === 'text') p.set('estmk_nr', this.billAddrDisplay || ''); else p.set('custnum', this.billAddrCustnum || '');
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?${p.toString()}`);
|
||
if (r.ok) {
|
||
const u = await r.json();
|
||
if (Array.isArray(u) && u.length < 6) this.checkOnlineState = true;
|
||
this.radiusUsers = Array.isArray(u) ? u : [];
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
this.isLoading = false;
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const connectParam = urlParams.get('connect');
|
||
if (connectParam && connectParam.toLowerCase() === 'true' && this.radiusUsers.length === 1) {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(this.radiusUsers[0].username)}`);
|
||
if (r.ok) {
|
||
const radacct = await r.json();
|
||
if (radacct && radacct.ip) {
|
||
this.onScanIp({ip: radacct.ip}, this.radiusUsers[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
this.searchCount++;
|
||
},
|
||
async fetchRadacctData(username) {
|
||
this.showRadacctModal = true;
|
||
this.radacctData = null;
|
||
try {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(username)}`);
|
||
if (r.ok) this.radacctData = await r.json();
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.radacctData = {};
|
||
}
|
||
},
|
||
async copy(text, event) {
|
||
if (!event || !event.currentTarget) return;
|
||
const btn = event.currentTarget;
|
||
if (btn.classList.contains('is-copied')) return;
|
||
await window.RadiusUtils.copyToClipboard(text);
|
||
btn.classList.add('is-copied');
|
||
btn.disabled = true;
|
||
setTimeout(() => {
|
||
btn.classList.remove('is-copied');
|
||
btn.disabled = false;
|
||
}, 1500);
|
||
},
|
||
clearFilters() {
|
||
this.billAddrDisplay = '';
|
||
this.billAddrCustnum = '';
|
||
this.username = '';
|
||
this.ip = '';
|
||
this.info = '';
|
||
this.radiusUsers = [];
|
||
this.hasSearched = false;
|
||
this.searchCount++;
|
||
this.visibleCount = 50;
|
||
},
|
||
loadMore() {
|
||
if (this.visibleCount < this.radiusUsers.length) this.visibleCount += 50;
|
||
},
|
||
async onScanIp(payload, item) {
|
||
const {ip} = payload;
|
||
const info = item.info;
|
||
if (!ip) return;
|
||
|
||
window.notify('info', `Starte Scan für ${ip}...`);
|
||
|
||
try {
|
||
const response = await fetch(`http://localhost:8094/scan?ip=${ip}`);
|
||
|
||
if (!response.ok) {
|
||
window.notify('error', `Scan-Server-Fehler: ${response.status}`);
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success' && data.url) {
|
||
const extensionId = this.extensionId;
|
||
const message = {
|
||
type: "INITIATE_ROUTER_LOGIN",
|
||
payload: {
|
||
ip: ip,
|
||
url: data.url,
|
||
info: info
|
||
}
|
||
};
|
||
|
||
if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
|
||
try {
|
||
chrome.runtime.sendMessage(extensionId, message, (response) => {
|
||
if (chrome.runtime.lastError) {
|
||
console.warn("Senden an Erweiterung fehlgeschlagen:", chrome.runtime.lastError.message);
|
||
window.notify('warning', 'Scan-Daten konnten nicht an die Erweiterung gesendet werden.');
|
||
} else {
|
||
console.log("Erweiterung hat geantwortet:", response);
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.error("Fehler beim Senden an die Erweiterung:", e);
|
||
window.notify('error', 'Fehler beim Senden an die Erweiterung.');
|
||
}
|
||
} else {
|
||
console.warn("Chrome Extension Messaging API nicht verfügbar.");
|
||
window.notify('warning', 'Chrome Messaging API nicht gefunden.');
|
||
}
|
||
|
||
} else if (data.status === 'not_found') {
|
||
window.notify('warning', `Kein Gerät für ${ip} gefunden.`);
|
||
} else {
|
||
window.notify('error', 'Ungültige Antwort vom Scan-Server.');
|
||
}
|
||
} catch (error) {
|
||
console.error('IP-Scan fehlgeschlagen:', error);
|
||
window.notify('error', `Scan für ${ip} fehlgeschlagen.`);
|
||
}
|
||
},
|
||
async openTransferModal(username) {
|
||
this.showTransferModal = true;
|
||
this.transferModalUsername = username;
|
||
this.transferYear = new Date().getFullYear();
|
||
this.transferMonth = new Date().getMonth() + 1;
|
||
await this.fetchTransferYearData();
|
||
},
|
||
closeTransferModal() {
|
||
this.showTransferModal = false;
|
||
this.transferModalUsername = '';
|
||
this.transferYearlyData = null;
|
||
this.transferMonthlyData = null;
|
||
this.showYearDropdown = false;
|
||
this.showEmailModal = false;
|
||
this.recipientEmail = '';
|
||
this.isSendingEmail = false;
|
||
if (this.transferChartInstance) {
|
||
this.transferChartInstance.destroy();
|
||
this.transferChartInstance = null;
|
||
}
|
||
},
|
||
async fetchTransferYearData() {
|
||
this.transferInitialLoading = true;
|
||
this.transferYearlyData = null;
|
||
try {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=0`);
|
||
if (r.ok) {
|
||
const d = await r.json();
|
||
if (d && d.monthlySummary) {
|
||
this.transferYearlyData = d;
|
||
const last = [...d.monthlySummary].reverse().find(m => m.grandTotalBytes > 0);
|
||
this.transferMonth = last ? last.month : new Date().getMonth() + 1;
|
||
await this.fetchTransferMonthData();
|
||
}
|
||
} else this.transferYearlyData = null;
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.transferYearlyData = null;
|
||
}
|
||
this.transferInitialLoading = false;
|
||
},
|
||
async fetchTransferMonthData() {
|
||
this.transferMonthlyLoading = true;
|
||
this.transferMonthlyData = null;
|
||
if (this.transferChartInstance) this.transferChartInstance.destroy();
|
||
try {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=${this.transferMonth}`);
|
||
this.transferMonthlyData = r.ok ? await r.json() : null;
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.transferMonthlyData = null;
|
||
}
|
||
this.transferMonthlyLoading = false;
|
||
this.$nextTick(() => {
|
||
if (this.showTransferModal) this.renderTransferChart();
|
||
});
|
||
},
|
||
prepareEmailModal() {
|
||
if (this.transferInitialLoading || !this.transferYearlyData || !this.transferYearlyData.yearlySummary || this.transferYearlyData.yearlySummary.grandTotalBytes === 0) return;
|
||
this.recipientEmail = '';
|
||
this.showEmailModal = true;
|
||
},
|
||
async sendTransferEmail() {
|
||
if (!this.transferMonthlyData || !this.transferChartInstance || !this.isValidEmail) return;
|
||
this.isSendingEmail = true;
|
||
try {
|
||
const chartImageBase64 = this.transferChartInstance.toBase64Image();
|
||
const payload = {
|
||
username: this.transferModalUsername,
|
||
year: this.transferYear,
|
||
month: this.transferMonth,
|
||
monthlySummary: this.transferMonthlyData.summary,
|
||
monthlyDetails: this.transferMonthlyData.details,
|
||
chartImage: chartImageBase64,
|
||
recipient: this.recipientEmail
|
||
};
|
||
const res = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/sendCustomerEmail`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
if (res.ok) {
|
||
window.notify('success', 'E-Mail wurde erfolgreich versendet.');
|
||
this.showEmailModal = false;
|
||
} else {
|
||
throw new Error('Server responded with an error.');
|
||
}
|
||
} catch (e) {
|
||
console.error("Failed to send transfer email:", e);
|
||
window.notify('error', 'Fehler beim Senden der E-Mail.');
|
||
} finally {
|
||
this.isSendingEmail = false;
|
||
}
|
||
},
|
||
isMonthDisabled(month) {
|
||
if (this.transferInitialLoading || this.transferMonthlyLoading) return true;
|
||
if (!this.transferYearlyData?.monthlySummary) return true;
|
||
const m = this.transferYearlyData.monthlySummary.find(m => m.month === month);
|
||
return !m || m.grandTotalBytes === 0;
|
||
},
|
||
selectYear(year) {
|
||
this.showYearDropdown = false;
|
||
if (this.transferYear !== year) this.changeTransferYear(year);
|
||
},
|
||
async changeTransferYear(year) {
|
||
this.transferYear = year;
|
||
await this.fetchTransferYearData();
|
||
},
|
||
async changeTransferMonth(month) {
|
||
this.transferMonth = month;
|
||
await this.fetchTransferMonthData();
|
||
},
|
||
processChartData(details) {
|
||
if (!details || !details.length) return {labels: [], datasets: []};
|
||
const daily = details.reduce((a, s) => {
|
||
const d = s.startTime.split(' ')[0];
|
||
if (!a[d]) a[d] = {downloadBytes: 0, uploadBytes: 0};
|
||
a[d].downloadBytes += Number(s.downloadBytes) || 0;
|
||
a[d].uploadBytes += Number(s.uploadBytes) || 0;
|
||
return a;
|
||
}, {});
|
||
const dates = Object.keys(daily).sort((a, b) => new Date(a) - new Date(b));
|
||
return {
|
||
labels: dates,
|
||
datasets: [{
|
||
label: 'Download',
|
||
data: dates.map(d => daily[d].downloadBytes),
|
||
borderColor: 'rgba(15, 157, 88, 0.8)',
|
||
backgroundColor: 'rgba(15, 157, 88, 0.1)',
|
||
fill: true,
|
||
tension: 0.3,
|
||
pointRadius: 2,
|
||
borderWidth: 1.5
|
||
}, {
|
||
label: 'Upload',
|
||
data: dates.map(d => daily[d].uploadBytes),
|
||
borderColor: 'rgba(0, 83, 132, 0.8)',
|
||
backgroundColor: 'rgba(0, 83, 132, 0.1)',
|
||
fill: true,
|
||
tension: 0.3,
|
||
pointRadius: 2,
|
||
borderWidth: 1.5
|
||
}]
|
||
};
|
||
},
|
||
renderTransferChart() {
|
||
if (this.transferChartInstance) this.transferChartInstance.destroy();
|
||
if (!this.$refs.transferChartCanvas || !this.transferMonthlyData?.details?.length || !window.Chart) return;
|
||
const d = this.processChartData(this.transferMonthlyData.details);
|
||
if (!d.labels.length) return;
|
||
const chartBackgroundColorPlugin = {
|
||
id: 'customCanvasBackgroundColor', beforeDraw: (chart) => {
|
||
const {ctx} = chart;
|
||
ctx.save();
|
||
ctx.globalCompositeOperation = 'destination-over';
|
||
ctx.fillStyle = '#ffffff';
|
||
ctx.fillRect(0, 0, chart.width, chart.height);
|
||
ctx.restore();
|
||
}
|
||
};
|
||
this.transferChartInstance = new Chart(this.$refs.transferChartCanvas.getContext('2d'), {
|
||
type: 'line',
|
||
data: d,
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: {
|
||
x: {
|
||
type: 'time',
|
||
time: {unit: 'day', tooltipFormat: 'DD.MM.YYYY', displayFormats: {day: 'DD.MM'}},
|
||
grid: {display: false},
|
||
ticks: {maxRotation: 0, autoSkip: true, maxTicksLimit: 15}
|
||
},
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {callback: (v) => window.RadiusUtils.formatBytes(v, 0)},
|
||
grid: {color: 'rgba(0,0,0,0.05)'}
|
||
}
|
||
},
|
||
plugins: {
|
||
tooltip: {callbacks: {label: (c) => `${c.dataset.label || ''}: ${window.RadiusUtils.formatBytes(c.parsed.y)}`}},
|
||
legend: {position: 'bottom', labels: {usePointStyle: true, boxWidth: 8, padding: 20}}
|
||
},
|
||
interaction: {mode: 'index', intersect: false}
|
||
},
|
||
plugins: [chartBackgroundColorPlugin]
|
||
});
|
||
}
|
||
}
|
||
});
|