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) {