From e6860461ee27244ad0ba8fe267335eac6ce664f8 Mon Sep 17 00:00:00 2001 From: Patrik Oberschmid Date: Fri, 10 Apr 2026 11:40:26 +0200 Subject: [PATCH] Extract presence module and add SVG status icons Move quick status buttons into dedicated presence.ts/css module with Font Awesome SVG icons (briefcase, headphones, grill, beer mug, screen-users). Bump version to 9.5.0. --- 3CX_TAPI.user.js | 211 +++++++++++++++++- client/package.json | 2 +- client/src/beer-mug-empty-regular-full.svg | 1 + client/src/briefcase-regular-full.svg | 1 + client/src/grill-hot-regular-full.svg | 1 + client/src/headphones-regular-full.svg | 1 + client/src/index.js | 5 + client/src/presence.css | 27 +++ client/src/presence.ts | 81 +++++++ .../src/screen-users-sharp-regular-full.svg | 1 + client/src/search.css | 8 +- client/src/search.ts | 2 + 12 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 client/src/beer-mug-empty-regular-full.svg create mode 100644 client/src/briefcase-regular-full.svg create mode 100644 client/src/grill-hot-regular-full.svg create mode 100644 client/src/headphones-regular-full.svg create mode 100644 client/src/presence.css create mode 100644 client/src/presence.ts create mode 100644 client/src/screen-users-sharp-regular-full.svg diff --git a/3CX_TAPI.user.js b/3CX_TAPI.user.js index e1ae490..dad2309 100644 --- a/3CX_TAPI.user.js +++ b/3CX_TAPI.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 3CX TAPI // @namespace http://cp-solutions.at -// @version 9.4.0 +// @version 9.5.0 // @author Daniel Triendl // @copyright Copyright CP Solutions GmbH // @source https://source.cp-austria.at/CPATRD/3cx_tapi.git @@ -122,6 +122,56 @@ module.exports = function (i) { return i[1]; }; +/***/ }, + +/***/ "./node_modules/css-loader/dist/cjs.js!./src/presence.css" +(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ A: () => (__WEBPACK_DEFAULT_EXPORT__) +/* harmony export */ }); +/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./node_modules/css-loader/dist/runtime/noSourceMaps.js"); +/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./node_modules/css-loader/dist/runtime/api.js"); +/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__); +// Imports + + +var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default())); +// Module +___CSS_LOADER_EXPORT___.push([module.id, `.tapi-quick-btn { + border: 1px solid transparent; + cursor: pointer; + padding: 3px; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.tapi-quick-btn svg { + width: 16px; + height: 16px; + fill: #fff; +} + +.tapi-btn-away { + background-color: #f0ad4e; +} + +.tapi-btn-dnd { + background-color: #d9534f; +} + +.tapi-btn-available { + background-color: #5cb85c; +} +`, ""]); +// Exports +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___); + + /***/ }, /***/ "./node_modules/css-loader/dist/cjs.js!./src/search.css" @@ -140,11 +190,17 @@ module.exports = function (i) { var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default())); // Module -___CSS_LOADER_EXPORT___.push([module.id, `.tapi-search-autocomplete { +___CSS_LOADER_EXPORT___.push([module.id, `.tapi-form { + display: inline-flex; + align-items: center; + gap: 6px; + margin-right: 20px; +} + +.tapi-search-autocomplete { /*the container must be positioned relative:*/ position: relative; display: inline-block; - margin-right: 20px; } .tapi-search-autocomplete input { border: 1px solid transparent; @@ -486,6 +542,41 @@ module.exports = styleTagTransform; /***/ }, +/***/ "./src/beer-mug-empty-regular-full.svg" +(module) { + +module.exports = "" + +/***/ }, + +/***/ "./src/briefcase-regular-full.svg" +(module) { + +module.exports = "" + +/***/ }, + +/***/ "./src/grill-hot-regular-full.svg" +(module) { + +module.exports = "" + +/***/ }, + +/***/ "./src/headphones-regular-full.svg" +(module) { + +module.exports = "" + +/***/ }, + +/***/ "./src/screen-users-sharp-regular-full.svg" +(module) { + +module.exports = "" + +/***/ }, + /***/ "./src/stopwatch-regular-full.svg" (module) { @@ -4458,9 +4549,9 @@ var insertStyleElement_default = /*#__PURE__*/__webpack_require__.n(insertStyleE // EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleTagTransform.js var styleTagTransform = __webpack_require__("./node_modules/style-loader/dist/runtime/styleTagTransform.js"); var styleTagTransform_default = /*#__PURE__*/__webpack_require__.n(styleTagTransform); -// EXTERNAL MODULE: ./node_modules/css-loader/dist/cjs.js!./src/search.css -var search = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/search.css"); -;// ./src/search.css +// EXTERNAL MODULE: ./node_modules/css-loader/dist/cjs.js!./src/presence.css +var presence = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/presence.css"); +;// ./src/presence.css @@ -4480,7 +4571,105 @@ options.insert = insertBySelector_default().bind(null, "head"); options.domAPI = (styleDomAPI_default()); options.insertStyleElement = (insertStyleElement_default()); -var update = injectStylesIntoStyleTag_default()(search/* default */.A, options); +var update = injectStylesIntoStyleTag_default()(presence/* default */.A, options); + + + + + /* harmony default export */ const src_presence = (presence/* default */.A && presence/* default */.A.locals ? presence/* default */.A.locals : undefined); + +;// ./src/presence.ts + + +const iconArbeiten = __webpack_require__("./src/briefcase-regular-full.svg"); +const iconBesprechung = __webpack_require__("./src/screen-users-sharp-regular-full.svg"); +const iconFokus = __webpack_require__("./src/headphones-regular-full.svg"); +const iconMittag = __webpack_require__("./src/grill-hot-regular-full.svg"); +const iconFeierabend = __webpack_require__("./src/beer-mug-empty-regular-full.svg"); +const QUICK_BUTTONS = [ + { icon: iconArbeiten, menuId: 'menuCustom1', message: '', css: 'tapi-btn-available', title: 'Arbeiten' }, + { icon: iconBesprechung, menuId: 'menuOutofoffice', message: 'Besprechung', css: 'tapi-btn-dnd', title: 'Besprechung' }, + { icon: iconFokus, menuId: 'menuOutofoffice', message: 'Fokus', css: 'tapi-btn-dnd', title: 'Fokus' }, + { icon: iconMittag, menuId: 'menuAway', message: 'Mittag', css: 'tapi-btn-away', title: 'Mittag' }, + { icon: iconFeierabend, menuId: 'menuAway', message: 'Feierabend', css: 'tapi-btn-away', title: 'Feierabend' }, +]; +class Presence { + createButtons(element) { + console.log('Create TAPI Presence'); + var form = document.getElementById('tapiForm'); + var searchBox = document.getElementById('tapiSearchBox'); + QUICK_BUTTONS.forEach(btn => { + var button = document.createElement('button'); + button.type = 'button'; + button.innerHTML = btn.icon; + button.classList.add('tapi-quick-btn'); + button.classList.add(btn.css); + button.title = btn.title; + button.onclick = () => { this.setStatus(btn.menuId, btn.message); }; + form.insertBefore(button, searchBox); + }); + } + delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + async setStatus(menuId, message) { + var accMenu = document.getElementsByTagName('wc-account-menu')[0]; + var avatar = accMenu.getElementsByTagName('app-avatar')[0]; + avatar.click(); + await this.delay(1000); + if (message !== '') { + var pencilBtn = document.getElementById(menuId + 'SetStatus'); + if (pencilBtn) { + pencilBtn.click(); + await this.delay(500); + var modalInput = document.querySelector('input[data-qa="input"][maxlength="128"]'); + if (modalInput) { + modalInput.value = message; + fireChangeEvents(modalInput); + await this.delay(300); + var okBtn = Array.from(document.querySelectorAll('button')).find(btn => btn.textContent && btn.textContent.trim() === 'OK' && btn.getBoundingClientRect().width > 0); + if (okBtn) { + okBtn.click(); + await this.delay(500); + } + } + } + } + var statusItem = document.getElementById(menuId); + if (!statusItem || statusItem.getBoundingClientRect().width === 0) { + avatar.click(); + await this.delay(1000); + statusItem = document.getElementById(menuId); + } + if (statusItem) { + statusItem.click(); + } + } +} + +// EXTERNAL MODULE: ./node_modules/css-loader/dist/cjs.js!./src/search.css +var search = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/search.css"); +;// ./src/search.css + + + + + + + + + + + +var search_options = {}; + +search_options.styleTagTransform = (styleTagTransform_default()); +search_options.setAttributes = (setAttributesWithoutAttributes_default()); +search_options.insert = insertBySelector_default().bind(null, "head"); +search_options.domAPI = (styleDomAPI_default()); +search_options.insertStyleElement = (insertStyleElement_default()); + +var search_update = injectStylesIntoStyleTag_default()(search/* default */.A, search_options); @@ -4510,6 +4699,8 @@ class Search { createSearchWindow(element) { console.log('Create TAPI Search'); var form = document.createElement('form'); + form.id = 'tapiForm'; + form.classList.add('tapi-form'); form.onsubmit = () => { var items = document.getElementsByClassName('tapi-search-autocomplete-active'); if (items.length === 0) { @@ -4862,12 +5053,18 @@ class Status { + console.log('script start'); const src_search_0 = new Search(); // eslint-disable-next-line no-undef waitForKeyElements('ongoing-call-button', element => { src_search_0.createSearchWindow(element); }, false); +const src_presence_0 = new Presence(); +// eslint-disable-next-line no-undef +waitForKeyElements('#tapiForm', element => { + src_presence_0.createButtons(element); +}, true); const callNotification = new CallNotification(); // eslint-disable-next-line no-undef waitForKeyElements('call-view', element => { diff --git a/client/package.json b/client/package.json index 377e8f7..9e379d6 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "3cx-tapi", "description": "3CX CP Tapi and Projectmanager integration", - "version": "9.4.0", + "version": "9.5.0", "author": { "name": "Daniel Triendl", "email": "d.triendl@cp-solutions.at" diff --git a/client/src/beer-mug-empty-regular-full.svg b/client/src/beer-mug-empty-regular-full.svg new file mode 100644 index 0000000..356fef7 --- /dev/null +++ b/client/src/beer-mug-empty-regular-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/briefcase-regular-full.svg b/client/src/briefcase-regular-full.svg new file mode 100644 index 0000000..fcb4c8d --- /dev/null +++ b/client/src/briefcase-regular-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/grill-hot-regular-full.svg b/client/src/grill-hot-regular-full.svg new file mode 100644 index 0000000..023a665 --- /dev/null +++ b/client/src/grill-hot-regular-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/headphones-regular-full.svg b/client/src/headphones-regular-full.svg new file mode 100644 index 0000000..fa78716 --- /dev/null +++ b/client/src/headphones-regular-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/index.js b/client/src/index.js index 210ffc0..2bf8e71 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -2,6 +2,7 @@ import * as chrono from 'chrono-node' import { CallHistory } from './call-history' import { CallNotification } from './call-notification' +import { Presence } from './presence' import { Search } from './search' import { Status } from './status' @@ -11,6 +12,10 @@ const search = new Search() // eslint-disable-next-line no-undef waitForKeyElements('ongoing-call-button', (element) => { search.createSearchWindow(element) }, false) +const presence = new Presence() +// eslint-disable-next-line no-undef +waitForKeyElements('#tapiForm', (element) => { presence.createButtons(element) }, true) + const callNotification = new CallNotification() // eslint-disable-next-line no-undef waitForKeyElements('call-view', (element) => { callNotification.showCallNotification(element) }, false) diff --git a/client/src/presence.css b/client/src/presence.css new file mode 100644 index 0000000..609c900 --- /dev/null +++ b/client/src/presence.css @@ -0,0 +1,27 @@ +.tapi-quick-btn { + border: 1px solid transparent; + cursor: pointer; + padding: 3px; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.tapi-quick-btn svg { + width: 16px; + height: 16px; + fill: #fff; +} + +.tapi-btn-away { + background-color: #f0ad4e; +} + +.tapi-btn-dnd { + background-color: #d9534f; +} + +.tapi-btn-available { + background-color: #5cb85c; +} diff --git a/client/src/presence.ts b/client/src/presence.ts new file mode 100644 index 0000000..b714dc7 --- /dev/null +++ b/client/src/presence.ts @@ -0,0 +1,81 @@ +import './presence.css' +import { fireChangeEvents } from './utils' + +const iconArbeiten = require('./briefcase-regular-full.svg') +const iconBesprechung = require('./screen-users-sharp-regular-full.svg') +const iconFokus = require('./headphones-regular-full.svg') +const iconMittag = require('./grill-hot-regular-full.svg') +const iconFeierabend = require('./beer-mug-empty-regular-full.svg') + +const QUICK_BUTTONS = [ + { icon: iconArbeiten, menuId: 'menuCustom1', message: '', css: 'tapi-btn-available', title: 'Arbeiten' }, + { icon: iconBesprechung, menuId: 'menuOutofoffice', message: 'Besprechung', css: 'tapi-btn-dnd', title: 'Besprechung' }, + { icon: iconFokus, menuId: 'menuOutofoffice', message: 'Fokus', css: 'tapi-btn-dnd', title: 'Fokus' }, + { icon: iconMittag, menuId: 'menuAway', message: 'Mittag', css: 'tapi-btn-away', title: 'Mittag' }, + { icon: iconFeierabend, menuId: 'menuAway', message: 'Feierabend', css: 'tapi-btn-away', title: 'Feierabend' }, +] + +export class Presence { + public createButtons (element: HTMLElement) { + console.log('Create TAPI Presence') + + var form = document.getElementById('tapiForm') + var searchBox = document.getElementById('tapiSearchBox') + + QUICK_BUTTONS.forEach(btn => { + var button = document.createElement('button') + button.type = 'button' + button.innerHTML = btn.icon + button.classList.add('tapi-quick-btn') + button.classList.add(btn.css) + button.title = btn.title + button.onclick = () => { this.setStatus(btn.menuId, btn.message) } + form.insertBefore(button, searchBox) + }) + } + + private delay (ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + private async setStatus (menuId: string, message: string) { + var accMenu = document.getElementsByTagName('wc-account-menu')[0] + var avatar = accMenu.getElementsByTagName('app-avatar')[0] as HTMLAnchorElement + + avatar.click() + await this.delay(1000) + + if (message !== '') { + var pencilBtn = document.getElementById(menuId + 'SetStatus') as HTMLElement + if (pencilBtn) { + pencilBtn.click() + await this.delay(500) + + var modalInput = document.querySelector('input[data-qa="input"][maxlength="128"]') as HTMLInputElement + if (modalInput) { + modalInput.value = message + fireChangeEvents(modalInput) + await this.delay(300) + + var okBtn = Array.from(document.querySelectorAll('button')).find(btn => + btn.textContent && btn.textContent.trim() === 'OK' && btn.getBoundingClientRect().width > 0 + ) as HTMLButtonElement + if (okBtn) { + okBtn.click() + await this.delay(500) + } + } + } + } + + var statusItem = document.getElementById(menuId) as HTMLElement + if (!statusItem || statusItem.getBoundingClientRect().width === 0) { + avatar.click() + await this.delay(1000) + statusItem = document.getElementById(menuId) as HTMLElement + } + if (statusItem) { + statusItem.click() + } + } +} diff --git a/client/src/screen-users-sharp-regular-full.svg b/client/src/screen-users-sharp-regular-full.svg new file mode 100644 index 0000000..24d0ed8 --- /dev/null +++ b/client/src/screen-users-sharp-regular-full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/search.css b/client/src/search.css index 7c27081..b4b8dd3 100644 --- a/client/src/search.css +++ b/client/src/search.css @@ -1,8 +1,14 @@ +.tapi-form { + display: inline-flex; + align-items: center; + gap: 6px; + margin-right: 20px; +} + .tapi-search-autocomplete { /*the container must be positioned relative:*/ position: relative; display: inline-block; - margin-right: 20px; } .tapi-search-autocomplete input { border: 1px solid transparent; diff --git a/client/src/search.ts b/client/src/search.ts index bca3ff7..e7d455c 100644 --- a/client/src/search.ts +++ b/client/src/search.ts @@ -12,6 +12,8 @@ export class Search { console.log('Create TAPI Search') var form = document.createElement('form') + form.id = 'tapiForm' + form.classList.add('tapi-form') form.onsubmit = () => { var items = document.getElementsByClassName('tapi-search-autocomplete-active') if (items.length === 0) {