Files
thetool/public/assets/js/xinon-vodia-identity.js

359 lines
15 KiB
JavaScript

document.body.insertAdjacentHTML('beforeend', `
<template id="vodia-identity-template">
<li id="vodia-identity-container" class="dropdown notification-list ml-2 my-auto" style="display: none;">
<a href="#" class="nav-link nav-user d-flex align-items-center" data-ref="callLookupButton" title="Aktuellen Anrufer nachschlagen" style="padding: 0!important;min-width: unset!important">
<span class="fa-stack" data-ref="callLookupIconStack">
<i class="fa-solid fa-phone fa-stack-1x" style="left: -5px; top: 6px;"></i>
<i class="fa-solid fa-magnifying-glass fa-stack-1x" style="transform: scale(0.7);"></i>
</span>
<i class="fas fa-spinner fa-spin d-none" data-ref="callLookupSpinner" style="margin-right: 10px"></i>
</a>
<a href="#" class="nav-link nav-user dropdown-toggle d-flex align-items-center" aria-haspopup="true" aria-expanded="false" data-ref="toggleButton">
<i class="phone-icon fas fa-phone" data-ref="phoneIcon"></i>
<div class="pro-user-name ml-2">
<div style="line-height: 1.2;">
<span>Ausgehende Identität: </span>
<span class="font-weight-bold" data-ref="currentName"></span>
<i class="far fa-chevron-down ml-2"></i>
</div>
<div class="small opacity-75" data-ref="currentNumber"></div>
</div>
</a>
<div class="dropdown-menu dropdown-menu-right shadow-lg" style="min-width: 320px;" data-ref="dropdownMenu">
<div class="dropdown-item noti-title"><h6 class="m-0" data-ref="dropdownTitle"></h6></div>
<ul class="list-group list-group-flush" data-ref="identityList"></ul>
</div>
</li>
</template>
<template id="vodia-list-item-template">
<li class="list-group-item list-group-item-action d-flex align-items-center">
<i class="vodia-list-item-icon fas fa-circle mr-3" data-ref="colorBlock"></i>
<div>
<span class="name font-weight-bold d-block" data-ref="name"></span>
<span class="number small text-muted" data-ref="numberDisplay"></span>
</div>
</li>
</template>
`);
class VodiaIdentitySwitcher {
// --- Configuration ---
API_BASE_URL = window.baseurl || '/';
CACHE_DURATION_MS = 60000; // 60 seconds
LOCK_TIMEOUT_MS = 5000; // 5 seconds for a request to complete
CACHE_KEY = 'vodiaIdentityCache';
LOCK_KEY = 'vodiaIdentityCache_lock';
TEXT = {
checking: "Prüfe...",
setting: "Ändere...",
dropdownTitle: "Ausgehende Identität wählen:",
ownExtension: "Eigene Nummer",
customIdentity: "Andere Nummer",
noActiveCall: "Kein aktiver Anruf gefunden.",
lookupError: "Fehler bei der Anrufabfrage.",
fetchError: "Fehler"
};
// --- State ---
elements = {};
templates = {};
constructor(parentElement) {
if (!parentElement) return;
this._initializeTemplates();
if (!this.templates.main || !this.templates.listItem) return;
this._createSwitcherUI(parentElement);
this._addEventListeners();
// Initial load if tab is already visible
if (document.visibilityState === 'visible') {
this.loadIdentity();
}
}
// --- Private Methods: Initialization ---
_initializeTemplates() {
this.templates.main = document.getElementById('vodia-identity-template');
this.templates.listItem = document.getElementById('vodia-list-item-template');
if (!this.templates.main || !this.templates.listItem) {
console.error("Vodia Switcher Error: Required HTML <template> tags not found.");
}
}
_createSwitcherUI(parentElement) {
const fragment = this.templates.main.content.cloneNode(true);
const container = fragment.querySelector('#vodia-identity-container');
this.elements = {
container,
callLookupButton: container.querySelector('[data-ref="callLookupButton"]'),
callLookupIconStack: container.querySelector('[data-ref="callLookupIconStack"]'),
callLookupSpinner: container.querySelector('[data-ref="callLookupSpinner"]'),
toggleButton: container.querySelector('[data-ref="toggleButton"]'),
phoneIcon: container.querySelector('[data-ref="phoneIcon"]'),
currentName: container.querySelector('[data-ref="currentName"]'),
currentNumber: container.querySelector('[data-ref="currentNumber"]'),
dropdownMenu: container.querySelector('[data-ref="dropdownMenu"]'),
identityList: container.querySelector('[data-ref="identityList"]'),
};
this.elements.dropdownMenu.querySelector('[data-ref="dropdownTitle"]').textContent = this.TEXT.dropdownTitle;
parentElement.prepend(fragment);
}
_addEventListeners() {
// Dropdown toggle
this.elements.toggleButton.addEventListener('click', e => {
e.preventDefault();
this.elements.container.classList.toggle('show');
this.elements.toggleButton.setAttribute('aria-expanded', this.elements.container.classList.contains('show'));
});
// Close dropdown on outside click
document.addEventListener('click', e => {
if (!this.elements.container.contains(e.target)) {
this.elements.container.classList.remove('show');
this.elements.toggleButton.setAttribute('aria-expanded', 'false');
}
});
// Call lookup button
this.elements.callLookupButton.addEventListener('click', e => {
e.preventDefault();
this._handleCallLookup();
});
// Reload data when tab becomes visible
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
this.loadIdentity();
}
});
// Sync state across tabs when another tab updates the cache
window.addEventListener('storage', e => {
if (e.key === this.CACHE_KEY && e.newValue) {
this._updateFromState(JSON.parse(e.newValue).data);
}
});
}
// --- Private Methods: API & Data Handling ---
_getCache(key) {
const cachedString = localStorage.getItem(key);
if (!cachedString) return null;
try {
return JSON.parse(cachedString);
} catch (e) {
localStorage.removeItem(key); // Clear corrupted cache
return null;
}
}
_setCache(key, data) {
try {
localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() }));
} catch (e) {
console.error(`Vodia Cache Error (${key}): Could not write to localStorage.`, e);
}
}
async _fetchJSON(endpoint, options = {}) {
const response = await fetch(`${this.API_BASE_URL}${endpoint}`, options);
if (!response.ok) throw new Error(`Network response was not ok (${response.status})`);
const data = await response.json();
if (data.status !== "OK") throw new Error(data.message || 'API returned an error');
return data;
}
async loadIdentity() {
// 1. Use fresh cache if available (handles re-focus)
const cached = this._getCache(this.CACHE_KEY);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION_MS) {
return this._updateFromState(cached.data);
}
// 2. Check for a lock from another tab to prevent multiple requests
const lock = this._getCache(this.LOCK_KEY);
if (lock && Date.now() - lock.timestamp < this.LOCK_TIMEOUT_MS) {
return; // Another tab is fetching, the 'storage' event will update this tab.
}
// 3. This tab will fetch the data. Set a lock.
this._setCache(this.LOCK_KEY, {}); // Set lock with current timestamp
this._renderState('loading');
try {
const { result } = await this._fetchJSON('User/Api/do=getVodiaIdentity');
this._setCache(this.CACHE_KEY, result); // This triggers 'storage' event for other tabs
this._updateFromState(result);
} catch (error) {
console.error("Vodia Fetch Error:", error.message);
this._renderState('error');
} finally {
localStorage.removeItem(this.LOCK_KEY); // Release lock
}
}
async setVodiaOutboundIdentity(number) {
this._renderState('setting');
this.elements.container.classList.remove('show');
this.elements.toggleButton.setAttribute('aria-expanded', 'false');
try {
await this._fetchJSON('User/Api/do=setVodiaIdentity', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ 'number': number })
});
// Clear cache and lock, then force reload across all tabs
localStorage.removeItem(this.CACHE_KEY);
localStorage.removeItem(this.LOCK_KEY);
this.loadIdentity();
} catch (error) {
console.error("Vodia Set Error:", error.message);
if (window.notify) window.notify('error', "Fehler beim Ändern der ID!");
this._renderState('error'); // Revert to error state, but previous data will be loaded on next focus
}
}
async _handleCallLookup() {
this._setLookupButtonLoadingState(true);
try {
const { result } = await this._fetchJSON('User/Api/do=getVodiaCall');
if (result.number && result.number.length >= 5) {
window.open(`${this.API_BASE_URL}Address/Index?filter[pfm]=${result.number}`, '_blank');
} else {
if (window.notify) window.notify('info', this.TEXT.noActiveCall);
}
} catch (error) {
console.error("Vodia Call Lookup Error:", error.message);
if (window.notify) window.notify('error', this.TEXT.lookupError);
} finally {
this._setLookupButtonLoadingState(false);
}
}
// --- Private Methods: UI Rendering ---
_setLookupButtonLoadingState(isLoading) {
this.elements.callLookupIconStack.classList.toggle('d-none', isLoading);
this.elements.callLookupSpinner.classList.toggle('d-none', !isLoading);
this.elements.callLookupButton.toggleAttribute('disabled', isLoading);
}
_renderState(state, message = '') {
const { phoneIcon, currentName, currentNumber } = this.elements;
phoneIcon.className = 'phone-icon fas fa-phone'; // Reset classes
currentNumber.textContent = '';
switch(state) {
case 'loading':
phoneIcon.classList.add('fa-spin', 'text-warning');
currentName.textContent = this.TEXT.checking;
break;
case 'setting':
phoneIcon.classList.add('fa-spin', 'text-warning');
currentName.textContent = this.TEXT.setting;
break;
case 'error':
phoneIcon.classList.add('text-danger');
currentName.textContent = this.TEXT.fetchError;
break;
case 'success':
phoneIcon.classList.add('text-success');
break;
}
}
_updateFromState(vodiaState) {
if (!vodiaState?.enabled) {
this.elements.container.style.display = 'none';
return;
}
this.elements.container.style.display = 'flex';
this._renderState('success');
this._updateCurrentIdentityDisplay(vodiaState);
this._renderIdentityList(vodiaState);
}
_updateCurrentIdentityDisplay(vodiaState) {
const { 'default': defaultDisplay, default_number, current, identities = {} } = vodiaState;
const currentId = current.replaceAll(' ', "");
const defaultId = default_number.replaceAll(' ', "");
let activeName = this.TEXT.customIdentity;
let activeNumberDisplay = `(${current})`;
if (currentId === defaultId) {
activeName = this.TEXT.ownExtension;
activeNumberDisplay = `(${defaultDisplay})`;
} else {
const foundName = Object.keys(identities).find(name => identities[name].number === currentId);
if (foundName) {
activeName = foundName;
activeNumberDisplay = `(${identities[foundName].display})`;
}
}
this.elements.currentName.textContent = activeName;
this.elements.currentNumber.textContent = activeNumberDisplay;
this.elements.toggleButton.title = `Aktive ID: ${activeName} ${activeNumberDisplay}`;
}
_renderIdentityList(vodiaState) {
this.elements.identityList.innerHTML = '';
const { 'default': defaultDisplay, default_number, current, identities = {} } = vodiaState;
const currentId = current.replaceAll(' ', "");
const defaultId = default_number.replaceAll(' ', "");
// Add own extension
this.elements.identityList.appendChild(this._createListItem({
name: this.TEXT.ownExtension,
number: defaultId,
display: defaultDisplay,
color: 'blue',
isActive: currentId === defaultId
}));
// Add other identities
for (const name in identities) {
const ident = identities[name];
this.elements.identityList.appendChild(this._createListItem({
name,
number: ident.number,
display: ident.display,
color: ident.color,
isActive: currentId === ident.number
}));
}
}
_createListItem({ name, number, display, color, isActive }) {
const fragment = this.templates.listItem.content.cloneNode(true);
const item = fragment.querySelector('li');
item.querySelector('[data-ref="colorBlock"]').className = `vodia-list-item-icon fas fa-circle mr-3 vodia-identity-color-${color || 'grey'}`;
item.querySelector('[data-ref="name"]').textContent = name;
item.querySelector('[data-ref="numberDisplay"]').textContent = display;
if (isActive) {
item.classList.add('active');
} else {
item.classList.add('pointer');
item.addEventListener('click', () => this.setVodiaOutboundIdentity(number));
}
return item;
}
}
// --- Bootstrap ---
document.addEventListener('DOMContentLoaded', () => {
const topbar = document.querySelector("#topbar");
if (topbar) new VodiaIdentitySwitcher(topbar);
});