Compare commits
10 Commits
v9.5.0
..
2874ea78c4
| Author | SHA1 | Date | |
|---|---|---|---|
| 2874ea78c4 | |||
| d5558b61b2 | |||
| b7a952ee81 | |||
| 523477ffba | |||
| 74992a020f | |||
| 235b7cba18 | |||
| 18c6f6f10e | |||
| 02c8e0ea3c | |||
| ccf1f63f1b | |||
| 8459f7938d |
@@ -0,0 +1,3 @@
|
|||||||
|
*Dockerfile*
|
||||||
|
*docker-compose*
|
||||||
|
node_modules
|
||||||
+472
-219
@@ -1,11 +1,11 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name 3CX TAPI
|
// @name 3CX TAPI
|
||||||
// @namespace http://cp-solutions.at
|
// @namespace http://cp-solutions.at
|
||||||
// @version 9.5.0
|
// @version 9.5.1
|
||||||
// @author Daniel Triendl <d.triendl@cp-solutions.at>
|
// @author Daniel Triendl <d.triendl@cp-solutions.at>
|
||||||
// @copyright Copyright CP Solutions GmbH
|
// @copyright Copyright CP Solutions GmbH
|
||||||
// @source https://source.cp-austria.at/CPATRD/3cx_tapi.git
|
// @source https://source.cp-austria.at/CPATRD/3cx_tapi.git
|
||||||
// @downloadURL https://source.cp-austria.at/CPATRD/3cx_tapi.git/raw/branch/master/3CX_TAPI.user.js
|
// @downloadURL https://3cxtapi.cp-austria.at/3CX_TAPI.user.js
|
||||||
// @match https://192.168.0.154:5001/*
|
// @match https://192.168.0.154:5001/*
|
||||||
// @match https://cpsolution.my3cx.at:5001/*
|
// @match https://cpsolution.my3cx.at:5001/*
|
||||||
// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js
|
// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js
|
||||||
@@ -122,6 +122,98 @@ module.exports = function (i) {
|
|||||||
return i[1];
|
return i[1];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/***/ },
|
||||||
|
|
||||||
|
/***/ "./node_modules/css-loader/dist/cjs.js!./src/availability.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-availability {
|
||||||
|
grid-column: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.7;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-availability small {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-dot-on {
|
||||||
|
background-color: #5cb85c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-dot-off {
|
||||||
|
background-color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-avatar-square {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 5px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
people-tile .avatar-size-m,
|
||||||
|
people-tile .avatar-size-m .avatar-container,
|
||||||
|
people-tile .avatar-size-m .avatar-content {
|
||||||
|
width: 57px !important;
|
||||||
|
height: 57px !important;
|
||||||
|
line-height: 57px !important;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
people-tile {
|
||||||
|
grid-template-rows: 29px 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
people-tile app-avatar {
|
||||||
|
grid-row: 1 / span 2 !important;
|
||||||
|
align-self: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-square-on {
|
||||||
|
background-color: #5cb85c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-square-off {
|
||||||
|
background-color: #d9534f;
|
||||||
|
}
|
||||||
|
`, ""]);
|
||||||
|
// Exports
|
||||||
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
|
||||||
|
|
||||||
|
|
||||||
/***/ },
|
/***/ },
|
||||||
|
|
||||||
/***/ "./node_modules/css-loader/dist/cjs.js!./src/presence.css"
|
/***/ "./node_modules/css-loader/dist/cjs.js!./src/presence.css"
|
||||||
@@ -658,6 +750,344 @@ var __webpack_exports__ = {};
|
|||||||
(() => {
|
(() => {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js
|
||||||
|
var injectStylesIntoStyleTag = __webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
|
||||||
|
var injectStylesIntoStyleTag_default = /*#__PURE__*/__webpack_require__.n(injectStylesIntoStyleTag);
|
||||||
|
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleDomAPI.js
|
||||||
|
var styleDomAPI = __webpack_require__("./node_modules/style-loader/dist/runtime/styleDomAPI.js");
|
||||||
|
var styleDomAPI_default = /*#__PURE__*/__webpack_require__.n(styleDomAPI);
|
||||||
|
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertBySelector.js
|
||||||
|
var insertBySelector = __webpack_require__("./node_modules/style-loader/dist/runtime/insertBySelector.js");
|
||||||
|
var insertBySelector_default = /*#__PURE__*/__webpack_require__.n(insertBySelector);
|
||||||
|
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js
|
||||||
|
var setAttributesWithoutAttributes = __webpack_require__("./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js");
|
||||||
|
var setAttributesWithoutAttributes_default = /*#__PURE__*/__webpack_require__.n(setAttributesWithoutAttributes);
|
||||||
|
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertStyleElement.js
|
||||||
|
var insertStyleElement = __webpack_require__("./node_modules/style-loader/dist/runtime/insertStyleElement.js");
|
||||||
|
var insertStyleElement_default = /*#__PURE__*/__webpack_require__.n(insertStyleElement);
|
||||||
|
// 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/availability.css
|
||||||
|
var availability = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/availability.css");
|
||||||
|
;// ./src/availability.css
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var options = {};
|
||||||
|
|
||||||
|
options.styleTagTransform = (styleTagTransform_default());
|
||||||
|
options.setAttributes = (setAttributesWithoutAttributes_default());
|
||||||
|
options.insert = insertBySelector_default().bind(null, "head");
|
||||||
|
options.domAPI = (styleDomAPI_default());
|
||||||
|
options.insertStyleElement = (insertStyleElement_default());
|
||||||
|
|
||||||
|
var update = injectStylesIntoStyleTag_default()(availability/* default */.A, options);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* harmony default export */ const src_availability = (availability/* default */.A && availability/* default */.A.locals ? availability/* default */.A.locals : undefined);
|
||||||
|
|
||||||
|
;// ./src/availability.ts
|
||||||
|
|
||||||
|
class Availability {
|
||||||
|
_service;
|
||||||
|
_availabilities = [];
|
||||||
|
constructor(service) {
|
||||||
|
this._service = service;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
this._service.subscribe((avs) => {
|
||||||
|
this._availabilities = avs;
|
||||||
|
this.updateAllIndicators();
|
||||||
|
});
|
||||||
|
waitForKeyElements('people-tile', (element) => { this.decorateTile(element); }, false);
|
||||||
|
}
|
||||||
|
decorateTile(tile) {
|
||||||
|
if (tile.querySelector('.tapi-availability')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var extEl = tile.querySelector('span[data-id="extPhone"]');
|
||||||
|
if (!extEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var extension = extEl.textContent.trim();
|
||||||
|
var indicator = document.createElement('div');
|
||||||
|
indicator.classList.add('tapi-availability');
|
||||||
|
indicator.dataset.tapiExtension = extension;
|
||||||
|
tile.appendChild(indicator);
|
||||||
|
this.updateIndicator(indicator);
|
||||||
|
var avatarContainer = tile.querySelector('app-avatar .avatar-container');
|
||||||
|
if (avatarContainer) {
|
||||||
|
var square = document.createElement('i');
|
||||||
|
square.classList.add('tapi-avatar-square');
|
||||||
|
square.dataset.tapiExtension = extension;
|
||||||
|
avatarContainer.appendChild(square);
|
||||||
|
this.updateSquare(square);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateAllIndicators() {
|
||||||
|
var indicators = document.getElementsByClassName('tapi-availability');
|
||||||
|
for (var i of indicators) {
|
||||||
|
this.updateIndicator(i);
|
||||||
|
}
|
||||||
|
var squares = document.getElementsByClassName('tapi-avatar-square');
|
||||||
|
for (var s of squares) {
|
||||||
|
this.updateSquare(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateIndicator(indicator) {
|
||||||
|
var extension = indicator.dataset.tapiExtension;
|
||||||
|
var entry = this._availabilities.find(a => a.extension === extension);
|
||||||
|
if (!entry) {
|
||||||
|
indicator.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dotClass = entry.loggedIn ? 'tapi-dot-on' : 'tapi-dot-off';
|
||||||
|
var time = '';
|
||||||
|
if (entry.lastStamp) {
|
||||||
|
var d = new Date(entry.lastStamp);
|
||||||
|
var pad = (n) => n.toString().padStart(2, '0');
|
||||||
|
time = pad(d.getDate()) + '.' + pad(d.getMonth() + 1) + '. ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||||
|
}
|
||||||
|
var location = '';
|
||||||
|
if (entry.loggedIn) {
|
||||||
|
location = entry.firma ? ' · Büro' : ' · Home';
|
||||||
|
}
|
||||||
|
indicator.innerHTML = '<span class="tapi-dot ' + dotClass + '"></span><small>' + time + location + '</small>';
|
||||||
|
}
|
||||||
|
updateSquare(square) {
|
||||||
|
var extension = square.dataset.tapiExtension;
|
||||||
|
var entry = this._availabilities.find(a => a.extension === extension);
|
||||||
|
square.classList.remove('tapi-square-on', 'tapi-square-off');
|
||||||
|
if (!entry) {
|
||||||
|
square.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
square.style.display = '';
|
||||||
|
square.classList.add(entry.loggedIn ? 'tapi-square-on' : 'tapi-square-off');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;// ./src/config.ts
|
||||||
|
class _Config {
|
||||||
|
tapi_server_url = 'https://3cxtapi.cp-austria.at';
|
||||||
|
}
|
||||||
|
const Config = new _Config();
|
||||||
|
|
||||||
|
;// ./node_modules/@trim21/gm-fetch/dist/index.mjs
|
||||||
|
function parseRawHeaders(h) {
|
||||||
|
const s = h.trim();
|
||||||
|
if (!s) {
|
||||||
|
return new Headers();
|
||||||
|
}
|
||||||
|
const array = s.split("\r\n").map((value) => {
|
||||||
|
let s = value.split(":");
|
||||||
|
return [s[0].trim(), s[1].trim()];
|
||||||
|
});
|
||||||
|
return new Headers(array);
|
||||||
|
}
|
||||||
|
function parseGMResponse(req, res) {
|
||||||
|
// workaround TamperMonkey bug(?) where sometimes response is string despite responseType being "blob"
|
||||||
|
const headers = parseRawHeaders(res.responseHeaders);
|
||||||
|
const body = typeof res.response === "string"
|
||||||
|
? new Blob([res.response], { type: headers.get("Content-Type") || "text/plain" })
|
||||||
|
: res.response;
|
||||||
|
return new ResImpl(body, {
|
||||||
|
statusCode: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers,
|
||||||
|
finalUrl: res.finalUrl,
|
||||||
|
redirected: res.finalUrl === req.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
class ResImpl {
|
||||||
|
constructor(body, init) {
|
||||||
|
this.rawBody = body;
|
||||||
|
this.init = init;
|
||||||
|
this.body = body.stream();
|
||||||
|
const { headers, statusCode, statusText, finalUrl, redirected } = init;
|
||||||
|
this.headers = headers;
|
||||||
|
this.status = statusCode;
|
||||||
|
this.statusText = statusText;
|
||||||
|
this.url = finalUrl;
|
||||||
|
this.type = "basic";
|
||||||
|
this.redirected = redirected;
|
||||||
|
this._bodyUsed = false;
|
||||||
|
}
|
||||||
|
get bodyUsed() {
|
||||||
|
return this._bodyUsed;
|
||||||
|
}
|
||||||
|
get ok() {
|
||||||
|
return this.status < 300;
|
||||||
|
}
|
||||||
|
arrayBuffer() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
this._bodyUsed = true;
|
||||||
|
return this.rawBody.arrayBuffer();
|
||||||
|
}
|
||||||
|
blob() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
this._bodyUsed = true;
|
||||||
|
// `slice` will use empty string as default value, so need to pass all arguments.
|
||||||
|
return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type));
|
||||||
|
}
|
||||||
|
clone() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
return new ResImpl(this.rawBody, this.init);
|
||||||
|
}
|
||||||
|
formData() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
this._bodyUsed = true;
|
||||||
|
return this.rawBody.text().then(decode);
|
||||||
|
}
|
||||||
|
async json() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'json' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
this._bodyUsed = true;
|
||||||
|
return JSON.parse(await this.rawBody.text());
|
||||||
|
}
|
||||||
|
text() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'text' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
this._bodyUsed = true;
|
||||||
|
return this.rawBody.text();
|
||||||
|
}
|
||||||
|
async bytes() {
|
||||||
|
if (this.bodyUsed) {
|
||||||
|
throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read");
|
||||||
|
}
|
||||||
|
this._bodyUsed = true;
|
||||||
|
return new Uint8Array(await this.rawBody.arrayBuffer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function decode(body) {
|
||||||
|
const form = new FormData();
|
||||||
|
body
|
||||||
|
.trim()
|
||||||
|
.split("&")
|
||||||
|
.forEach(function (bytes) {
|
||||||
|
if (bytes) {
|
||||||
|
const split = bytes.split("=");
|
||||||
|
const name = split.shift()?.replace(/\+/g, " ");
|
||||||
|
const value = split.join("=").replace(/\+/g, " ");
|
||||||
|
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GM_fetch(input, init) {
|
||||||
|
const request = new Request(input, init);
|
||||||
|
let data;
|
||||||
|
if (init?.body) {
|
||||||
|
data = await request.text();
|
||||||
|
}
|
||||||
|
return await XHR(request, init, data);
|
||||||
|
}
|
||||||
|
function XHR(request, init, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (request.signal && request.signal.aborted) {
|
||||||
|
return reject(new DOMException("Aborted", "AbortError"));
|
||||||
|
}
|
||||||
|
GM.xmlHttpRequest({
|
||||||
|
url: request.url,
|
||||||
|
method: gmXHRMethod(request.method.toUpperCase()),
|
||||||
|
headers: Object.fromEntries(new Headers(init?.headers).entries()),
|
||||||
|
data: data,
|
||||||
|
responseType: "blob",
|
||||||
|
onload(res) {
|
||||||
|
try {
|
||||||
|
resolve(parseGMResponse(request, res));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onabort() {
|
||||||
|
reject(new DOMException("Aborted", "AbortError"));
|
||||||
|
},
|
||||||
|
ontimeout() {
|
||||||
|
reject(new TypeError("Network request failed, timeout"));
|
||||||
|
},
|
||||||
|
onerror(err) {
|
||||||
|
reject(new TypeError("Failed to fetch: " + err.finalUrl));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"];
|
||||||
|
// a ts type helper to narrow type
|
||||||
|
function includes(array, element) {
|
||||||
|
return array.includes(element);
|
||||||
|
}
|
||||||
|
function gmXHRMethod(method) {
|
||||||
|
if (includes(httpMethods, method)) {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
throw new Error(`unsupported http method ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//# sourceMappingURL=index.mjs.map
|
||||||
|
|
||||||
|
;// ./src/availability-service.ts
|
||||||
|
|
||||||
|
|
||||||
|
class AvailabilityService {
|
||||||
|
_availabilities = [];
|
||||||
|
_listeners = [];
|
||||||
|
_started = false;
|
||||||
|
start() {
|
||||||
|
if (this._started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._started = true;
|
||||||
|
this.fetch();
|
||||||
|
setInterval(() => this.fetch(), 30000);
|
||||||
|
}
|
||||||
|
subscribe(listener) {
|
||||||
|
this._listeners.push(listener);
|
||||||
|
if (this._availabilities.length > 0) {
|
||||||
|
listener(this._availabilities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get availabilities() {
|
||||||
|
return this._availabilities;
|
||||||
|
}
|
||||||
|
async fetch() {
|
||||||
|
try {
|
||||||
|
var response = await GM_fetch(Config.tapi_server_url + '/availability');
|
||||||
|
if (response.status === 200) {
|
||||||
|
this._availabilities = await response.json();
|
||||||
|
this._listeners.forEach(l => l(this._availabilities));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
;// ./node_modules/chrono-node/dist/esm/types.js
|
;// ./node_modules/chrono-node/dist/esm/types.js
|
||||||
var Meridiem;
|
var Meridiem;
|
||||||
(function (Meridiem) {
|
(function (Meridiem) {
|
||||||
@@ -4248,178 +4678,6 @@ function fireChangeEvents(element) {
|
|||||||
console.debug('change event dispatched for element: ' + element.id);
|
console.debug('change event dispatched for element: ' + element.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
;// ./node_modules/@trim21/gm-fetch/dist/index.mjs
|
|
||||||
function parseRawHeaders(h) {
|
|
||||||
const s = h.trim();
|
|
||||||
if (!s) {
|
|
||||||
return new Headers();
|
|
||||||
}
|
|
||||||
const array = s.split("\r\n").map((value) => {
|
|
||||||
let s = value.split(":");
|
|
||||||
return [s[0].trim(), s[1].trim()];
|
|
||||||
});
|
|
||||||
return new Headers(array);
|
|
||||||
}
|
|
||||||
function parseGMResponse(req, res) {
|
|
||||||
// workaround TamperMonkey bug(?) where sometimes response is string despite responseType being "blob"
|
|
||||||
const headers = parseRawHeaders(res.responseHeaders);
|
|
||||||
const body = typeof res.response === "string"
|
|
||||||
? new Blob([res.response], { type: headers.get("Content-Type") || "text/plain" })
|
|
||||||
: res.response;
|
|
||||||
return new ResImpl(body, {
|
|
||||||
statusCode: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers,
|
|
||||||
finalUrl: res.finalUrl,
|
|
||||||
redirected: res.finalUrl === req.url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
class ResImpl {
|
|
||||||
constructor(body, init) {
|
|
||||||
this.rawBody = body;
|
|
||||||
this.init = init;
|
|
||||||
this.body = body.stream();
|
|
||||||
const { headers, statusCode, statusText, finalUrl, redirected } = init;
|
|
||||||
this.headers = headers;
|
|
||||||
this.status = statusCode;
|
|
||||||
this.statusText = statusText;
|
|
||||||
this.url = finalUrl;
|
|
||||||
this.type = "basic";
|
|
||||||
this.redirected = redirected;
|
|
||||||
this._bodyUsed = false;
|
|
||||||
}
|
|
||||||
get bodyUsed() {
|
|
||||||
return this._bodyUsed;
|
|
||||||
}
|
|
||||||
get ok() {
|
|
||||||
return this.status < 300;
|
|
||||||
}
|
|
||||||
arrayBuffer() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
this._bodyUsed = true;
|
|
||||||
return this.rawBody.arrayBuffer();
|
|
||||||
}
|
|
||||||
blob() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
this._bodyUsed = true;
|
|
||||||
// `slice` will use empty string as default value, so need to pass all arguments.
|
|
||||||
return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type));
|
|
||||||
}
|
|
||||||
clone() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
return new ResImpl(this.rawBody, this.init);
|
|
||||||
}
|
|
||||||
formData() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
this._bodyUsed = true;
|
|
||||||
return this.rawBody.text().then(decode);
|
|
||||||
}
|
|
||||||
async json() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'json' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
this._bodyUsed = true;
|
|
||||||
return JSON.parse(await this.rawBody.text());
|
|
||||||
}
|
|
||||||
text() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'text' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
this._bodyUsed = true;
|
|
||||||
return this.rawBody.text();
|
|
||||||
}
|
|
||||||
async bytes() {
|
|
||||||
if (this.bodyUsed) {
|
|
||||||
throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read");
|
|
||||||
}
|
|
||||||
this._bodyUsed = true;
|
|
||||||
return new Uint8Array(await this.rawBody.arrayBuffer());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function decode(body) {
|
|
||||||
const form = new FormData();
|
|
||||||
body
|
|
||||||
.trim()
|
|
||||||
.split("&")
|
|
||||||
.forEach(function (bytes) {
|
|
||||||
if (bytes) {
|
|
||||||
const split = bytes.split("=");
|
|
||||||
const name = split.shift()?.replace(/\+/g, " ");
|
|
||||||
const value = split.join("=").replace(/\+/g, " ");
|
|
||||||
form.append(decodeURIComponent(name), decodeURIComponent(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function GM_fetch(input, init) {
|
|
||||||
const request = new Request(input, init);
|
|
||||||
let data;
|
|
||||||
if (init?.body) {
|
|
||||||
data = await request.text();
|
|
||||||
}
|
|
||||||
return await XHR(request, init, data);
|
|
||||||
}
|
|
||||||
function XHR(request, init, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (request.signal && request.signal.aborted) {
|
|
||||||
return reject(new DOMException("Aborted", "AbortError"));
|
|
||||||
}
|
|
||||||
GM.xmlHttpRequest({
|
|
||||||
url: request.url,
|
|
||||||
method: gmXHRMethod(request.method.toUpperCase()),
|
|
||||||
headers: Object.fromEntries(new Headers(init?.headers).entries()),
|
|
||||||
data: data,
|
|
||||||
responseType: "blob",
|
|
||||||
onload(res) {
|
|
||||||
try {
|
|
||||||
resolve(parseGMResponse(request, res));
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onabort() {
|
|
||||||
reject(new DOMException("Aborted", "AbortError"));
|
|
||||||
},
|
|
||||||
ontimeout() {
|
|
||||||
reject(new TypeError("Network request failed, timeout"));
|
|
||||||
},
|
|
||||||
onerror(err) {
|
|
||||||
reject(new TypeError("Failed to fetch: " + err.finalUrl));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"];
|
|
||||||
// a ts type helper to narrow type
|
|
||||||
function includes(array, element) {
|
|
||||||
return array.includes(element);
|
|
||||||
}
|
|
||||||
function gmXHRMethod(method) {
|
|
||||||
if (includes(httpMethods, method)) {
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
throw new Error(`unsupported http method ${method}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=index.mjs.map
|
|
||||||
|
|
||||||
;// ./src/config.ts
|
|
||||||
class _Config {
|
|
||||||
tapi_server_url = 'https://3cxtapi.cp-austria.at';
|
|
||||||
}
|
|
||||||
const Config = new _Config();
|
|
||||||
|
|
||||||
;// ./src/call-history.ts
|
;// ./src/call-history.ts
|
||||||
|
|
||||||
|
|
||||||
@@ -4531,24 +4789,6 @@ class CallNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js
|
|
||||||
var injectStylesIntoStyleTag = __webpack_require__("./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
|
|
||||||
var injectStylesIntoStyleTag_default = /*#__PURE__*/__webpack_require__.n(injectStylesIntoStyleTag);
|
|
||||||
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/styleDomAPI.js
|
|
||||||
var styleDomAPI = __webpack_require__("./node_modules/style-loader/dist/runtime/styleDomAPI.js");
|
|
||||||
var styleDomAPI_default = /*#__PURE__*/__webpack_require__.n(styleDomAPI);
|
|
||||||
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertBySelector.js
|
|
||||||
var insertBySelector = __webpack_require__("./node_modules/style-loader/dist/runtime/insertBySelector.js");
|
|
||||||
var insertBySelector_default = /*#__PURE__*/__webpack_require__.n(insertBySelector);
|
|
||||||
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js
|
|
||||||
var setAttributesWithoutAttributes = __webpack_require__("./node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js");
|
|
||||||
var setAttributesWithoutAttributes_default = /*#__PURE__*/__webpack_require__.n(setAttributesWithoutAttributes);
|
|
||||||
// EXTERNAL MODULE: ./node_modules/style-loader/dist/runtime/insertStyleElement.js
|
|
||||||
var insertStyleElement = __webpack_require__("./node_modules/style-loader/dist/runtime/insertStyleElement.js");
|
|
||||||
var insertStyleElement_default = /*#__PURE__*/__webpack_require__.n(insertStyleElement);
|
|
||||||
// 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/presence.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");
|
var presence = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/presence.css");
|
||||||
;// ./src/presence.css
|
;// ./src/presence.css
|
||||||
@@ -4563,15 +4803,15 @@ var presence = __webpack_require__("./node_modules/css-loader/dist/cjs.js!./src/
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
var options = {};
|
var presence_options = {};
|
||||||
|
|
||||||
options.styleTagTransform = (styleTagTransform_default());
|
presence_options.styleTagTransform = (styleTagTransform_default());
|
||||||
options.setAttributes = (setAttributesWithoutAttributes_default());
|
presence_options.setAttributes = (setAttributesWithoutAttributes_default());
|
||||||
options.insert = insertBySelector_default().bind(null, "head");
|
presence_options.insert = insertBySelector_default().bind(null, "head");
|
||||||
options.domAPI = (styleDomAPI_default());
|
presence_options.domAPI = (styleDomAPI_default());
|
||||||
options.insertStyleElement = (insertStyleElement_default());
|
presence_options.insertStyleElement = (insertStyleElement_default());
|
||||||
|
|
||||||
var update = injectStylesIntoStyleTag_default()(presence/* default */.A, options);
|
var presence_update = injectStylesIntoStyleTag_default()(presence/* default */.A, presence_options);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -4864,32 +5104,44 @@ var status_update = injectStylesIntoStyleTag_default()(cjs_js_src_status/* defau
|
|||||||
|
|
||||||
;// ./src/status.ts
|
;// ./src/status.ts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const zcIcon = __webpack_require__("./src/stopwatch-regular-full.svg");
|
const zcIcon = __webpack_require__("./src/stopwatch-regular-full.svg");
|
||||||
class Status {
|
class Status {
|
||||||
|
_service;
|
||||||
_user;
|
_user;
|
||||||
_enabled = false;
|
_enabled = false;
|
||||||
_statusOn = 'menuAvailable';
|
_statusOn = 'menuAvailable';
|
||||||
_statusOff = 'menuAway';
|
_statusOff = 'menuAway';
|
||||||
_currentStatus = undefined;
|
_currentStatus = undefined;
|
||||||
|
_subscribed = false;
|
||||||
|
constructor(service) {
|
||||||
|
this._service = service;
|
||||||
|
}
|
||||||
async showStatus(element) {
|
async showStatus(element) {
|
||||||
this._user = await GM.getValue('tapi-zc-user', '');
|
this._user = await GM.getValue('tapi-zc-user', '');
|
||||||
this._enabled = await GM.getValue('tapi-zc-enabled', false);
|
this._enabled = await GM.getValue('tapi-zc-enabled', false);
|
||||||
this._statusOn = await GM.getValue('tapi-zc-on', 'menuAvailable');
|
this._statusOn = await GM.getValue('tapi-zc-on', 'menuAvailable');
|
||||||
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
|
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
|
||||||
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
|
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
|
||||||
this.checkStatus();
|
this.subscribeOnce();
|
||||||
waitForKeyElements("wc-account-menu > div > ul", (element) => { this.addZcStatusPopup(element); }, true);
|
waitForKeyElements("wc-account-menu > div > ul", (element) => { this.addZcStatusPopup(element); }, true);
|
||||||
}
|
}
|
||||||
async checkStatus() {
|
subscribeOnce() {
|
||||||
if (this._enabled) {
|
if (this._subscribed) {
|
||||||
try {
|
return;
|
||||||
var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
|
}
|
||||||
if (response.status == 200) {
|
this._subscribed = true;
|
||||||
var status = await response.json();
|
this._service.subscribe((avs) => this.onAvailabilities(avs));
|
||||||
if (this._currentStatus !== status.loggedIn) {
|
}
|
||||||
this._currentStatus = status.loggedIn;
|
onAvailabilities(avs) {
|
||||||
|
if (!this._enabled || !this._user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var entry = avs.find(a => a.user === this._user);
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._currentStatus !== entry.loggedIn) {
|
||||||
|
this._currentStatus = entry.loggedIn;
|
||||||
console.log('New status, loggedIn', this._currentStatus);
|
console.log('New status, loggedIn', this._currentStatus);
|
||||||
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
||||||
var avatar = accMenu.getElementsByTagName("app-avatar")[0];
|
var avatar = accMenu.getElementsByTagName("app-avatar")[0];
|
||||||
@@ -4901,13 +5153,6 @@ class Status {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
setTimeout(() => this.checkStatus(), 30000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addZcStatusPopup(element) {
|
addZcStatusPopup(element) {
|
||||||
var divider = document.createElement('li');
|
var divider = document.createElement('li');
|
||||||
divider.classList.add('divider');
|
divider.classList.add('divider');
|
||||||
@@ -4998,7 +5243,9 @@ class Status {
|
|||||||
GM.setValue('tapi-zc-enabled', this._enabled);
|
GM.setValue('tapi-zc-enabled', this._enabled);
|
||||||
console.log('tapi-zc-enabled', this._enabled);
|
console.log('tapi-zc-enabled', this._enabled);
|
||||||
this._currentStatus = undefined;
|
this._currentStatus = undefined;
|
||||||
this.checkStatus();
|
if (this._enabled) {
|
||||||
|
this.onAvailabilities(this._service.availabilities);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
var zcOn = document.getElementById('tapi-zc-on');
|
var zcOn = document.getElementById('tapi-zc-on');
|
||||||
zcOn.value = this._statusOn;
|
zcOn.value = this._statusOn;
|
||||||
@@ -5054,6 +5301,8 @@ class Status {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log('script start');
|
console.log('script start');
|
||||||
const src_search_0 = new Search();
|
const src_search_0 = new Search();
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
@@ -5075,7 +5324,9 @@ const callHistory = new CallHistory();
|
|||||||
waitForKeyElements('call', element => {
|
waitForKeyElements('call', element => {
|
||||||
callHistory.showCallHistory(element);
|
callHistory.showCallHistory(element);
|
||||||
}, false);
|
}, false);
|
||||||
const src_status_0 = new Status();
|
const availabilityService = new AvailabilityService();
|
||||||
|
availabilityService.start();
|
||||||
|
const src_status_0 = new Status(availabilityService);
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
waitForKeyElements('wc-account-menu', element => {
|
waitForKeyElements('wc-account-menu', element => {
|
||||||
src_status_0.showStatus(element);
|
src_status_0.showStatus(element);
|
||||||
@@ -5083,6 +5334,8 @@ waitForKeyElements('wc-account-menu', element => {
|
|||||||
waitForKeyElements('wc-account-menu i.status-indicator', element => {
|
waitForKeyElements('wc-account-menu i.status-indicator', element => {
|
||||||
src_status_0.watchStatus(element);
|
src_status_0.watchStatus(element);
|
||||||
}, false);
|
}, false);
|
||||||
|
const src_availability_0 = new Availability(availabilityService);
|
||||||
|
src_availability_0.start();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/******/ })()
|
/******/ })()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module.exports = {
|
|||||||
author: pkg.author,
|
author: pkg.author,
|
||||||
copyright: 'Copyright CP Solutions GmbH',
|
copyright: 'Copyright CP Solutions GmbH',
|
||||||
source: pkg.repository.url,
|
source: pkg.repository.url,
|
||||||
downloadURL: `${pkg.repository.url}/raw/branch/master/3CX_TAPI.user.js`,
|
downloadURL: 'https://3cxtapi.cp-austria.at/3CX_TAPI.user.js',
|
||||||
match: [
|
match: [
|
||||||
'https://192.168.0.154:5001/*',
|
'https://192.168.0.154:5001/*',
|
||||||
'https://cpsolution.my3cx.at:5001/*'
|
'https://cpsolution.my3cx.at:5001/*'
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "3cx-tapi",
|
"name": "3cx-tapi",
|
||||||
"version": "9.3.0",
|
"version": "9.6.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "3cx-tapi",
|
"name": "3cx-tapi",
|
||||||
"version": "9.3.0",
|
"version": "9.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trim21/gm-fetch": "^0.3.0",
|
"@trim21/gm-fetch": "^0.3.0",
|
||||||
"chrono-node": "^2.9.0"
|
"chrono-node": "^2.9.0"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "3cx-tapi",
|
"name": "3cx-tapi",
|
||||||
"description": "3CX CP Tapi and Projectmanager integration",
|
"description": "3CX CP Tapi and Projectmanager integration",
|
||||||
"version": "9.5.0",
|
"version": "9.6.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Daniel Triendl",
|
"name": "Daniel Triendl",
|
||||||
"email": "d.triendl@cp-solutions.at"
|
"email": "d.triendl@cp-solutions.at"
|
||||||
|
|||||||
+2
-6
@@ -38,15 +38,11 @@ just install a package and import it in your js file. webpack will pack them wit
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
`dist/index.prod.user.js` is the final script.
|
`dist/3CX TAPI.prod.user.js` is the final script.
|
||||||
|
|
||||||
## distribution
|
## distribution
|
||||||
|
|
||||||
```
|
Userscript is included in server docker image
|
||||||
cp "dist/3CX TAPI.prod.user.js" ../3CX_TAPI.user.js
|
|
||||||
```
|
|
||||||
|
|
||||||
And commit 3CX_TAPI.user.js
|
|
||||||
|
|
||||||
## see also
|
## see also
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class AvailabilityInfo {
|
||||||
|
public user: string;
|
||||||
|
public loggedIn: boolean;
|
||||||
|
public extension: string;
|
||||||
|
public lastStamp: Date;
|
||||||
|
public atCompany: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Config } from './config'
|
||||||
|
import { AvailabilityInfo } from './availability-info'
|
||||||
|
import GM_fetch from '@trim21/gm-fetch'
|
||||||
|
|
||||||
|
type Listener = (availabilities: AvailabilityInfo[]) => void
|
||||||
|
|
||||||
|
export class AvailabilityService {
|
||||||
|
private _availabilities: AvailabilityInfo[] = []
|
||||||
|
private _listeners: Listener[] = []
|
||||||
|
private _started = false
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
if (this._started) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._started = true
|
||||||
|
this.fetch()
|
||||||
|
setInterval(() => this.fetch(), 30000)
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribe(listener: Listener) {
|
||||||
|
this._listeners.push(listener)
|
||||||
|
if (this._availabilities.length > 0) {
|
||||||
|
listener(this._availabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get availabilities(): AvailabilityInfo[] {
|
||||||
|
return this._availabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetch() {
|
||||||
|
try {
|
||||||
|
var response = await GM_fetch(Config.tapi_server_url + '/availability')
|
||||||
|
if (response.status === 200) {
|
||||||
|
var raw = await response.json() as AvailabilityInfo[]
|
||||||
|
this._availabilities = raw.map(a => ({
|
||||||
|
...a,
|
||||||
|
lastStamp: a.lastStamp ? new Date(a.lastStamp) : null,
|
||||||
|
}))
|
||||||
|
this._listeners.forEach(l => l(this._availabilities))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.tapi-availability {
|
||||||
|
grid-column: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.7;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-availability small {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-dot-on {
|
||||||
|
background-color: #5cb85c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-dot-off {
|
||||||
|
background-color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-avatar-square {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 5px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
people-tile .avatar-size-m,
|
||||||
|
people-tile .avatar-size-m .avatar-container,
|
||||||
|
people-tile .avatar-size-m .avatar-content {
|
||||||
|
width: 57px !important;
|
||||||
|
height: 57px !important;
|
||||||
|
line-height: 57px !important;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
people-tile {
|
||||||
|
grid-template-rows: 29px 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
people-tile app-avatar {
|
||||||
|
grid-row: 1 / span 2 !important;
|
||||||
|
align-self: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-square-on {
|
||||||
|
background-color: #5cb85c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapi-square-off {
|
||||||
|
background-color: #d9534f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import './availability.css'
|
||||||
|
import { AvailabilityInfo } from './availability-info'
|
||||||
|
import { AvailabilityService } from './availability-service'
|
||||||
|
|
||||||
|
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
|
||||||
|
|
||||||
|
export class Availability {
|
||||||
|
private _service: AvailabilityService
|
||||||
|
private _availabilities: AvailabilityInfo[] = []
|
||||||
|
|
||||||
|
constructor(service: AvailabilityService) {
|
||||||
|
this._service = service
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
this._service.subscribe((avs) => {
|
||||||
|
this._availabilities = avs
|
||||||
|
this.updateAllIndicators()
|
||||||
|
})
|
||||||
|
waitForKeyElements('people-tile', (element: HTMLElement) => { this.decorateTile(element) }, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private decorateTile(tile: HTMLElement) {
|
||||||
|
if (tile.querySelector('.tapi-availability')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var extEl = tile.querySelector('span[data-id="extPhone"]') as HTMLElement
|
||||||
|
if (!extEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var extension = extEl.textContent.trim()
|
||||||
|
|
||||||
|
var indicator = document.createElement('div')
|
||||||
|
indicator.classList.add('tapi-availability')
|
||||||
|
indicator.dataset.tapiExtension = extension
|
||||||
|
tile.appendChild(indicator)
|
||||||
|
this.updateIndicator(indicator)
|
||||||
|
|
||||||
|
var avatarContainer = tile.querySelector('app-avatar .avatar-container') as HTMLElement
|
||||||
|
if (avatarContainer) {
|
||||||
|
var square = document.createElement('i')
|
||||||
|
square.classList.add('tapi-avatar-square')
|
||||||
|
square.dataset.tapiExtension = extension
|
||||||
|
avatarContainer.appendChild(square)
|
||||||
|
this.updateSquare(square)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAllIndicators() {
|
||||||
|
var indicators = document.getElementsByClassName('tapi-availability')
|
||||||
|
for (var i of indicators) {
|
||||||
|
this.updateIndicator(i as HTMLElement)
|
||||||
|
}
|
||||||
|
var squares = document.getElementsByClassName('tapi-avatar-square')
|
||||||
|
for (var s of squares) {
|
||||||
|
this.updateSquare(s as HTMLElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateIndicator(indicator: HTMLElement) {
|
||||||
|
var extension = indicator.dataset.tapiExtension
|
||||||
|
var entry = this._availabilities.find(a => a.extension === extension)
|
||||||
|
if (!entry) {
|
||||||
|
indicator.innerHTML = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var dotClass = entry.loggedIn ? 'tapi-dot-on' : 'tapi-dot-off'
|
||||||
|
var time = ''
|
||||||
|
if (entry.lastStamp) {
|
||||||
|
var pad = (n: number) => n.toString().padStart(2, '0')
|
||||||
|
var d = entry.lastStamp
|
||||||
|
time = pad(d.getDate()) + '.' + pad(d.getMonth() + 1) + '. ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||||
|
}
|
||||||
|
var location = ''
|
||||||
|
if (entry.loggedIn) {
|
||||||
|
location = entry.atCompany ? ' · Büro' : ' · Home'
|
||||||
|
}
|
||||||
|
indicator.innerHTML = '<span class="tapi-dot ' + dotClass + '"></span><small>' + time + location + '</small>'
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSquare(square: HTMLElement) {
|
||||||
|
var extension = square.dataset.tapiExtension
|
||||||
|
var entry = this._availabilities.find(a => a.extension === extension)
|
||||||
|
square.classList.remove('tapi-square-on', 'tapi-square-off')
|
||||||
|
if (!entry) {
|
||||||
|
square.style.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
square.style.display = ''
|
||||||
|
square.classList.add(entry.loggedIn ? 'tapi-square-on' : 'tapi-square-off')
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-1
@@ -1,5 +1,7 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import * as chrono from 'chrono-node'
|
import * as chrono from 'chrono-node'
|
||||||
|
import { Availability } from './availability'
|
||||||
|
import { AvailabilityService } from './availability-service'
|
||||||
import { CallHistory } from './call-history'
|
import { CallHistory } from './call-history'
|
||||||
import { CallNotification } from './call-notification'
|
import { CallNotification } from './call-notification'
|
||||||
import { Presence } from './presence'
|
import { Presence } from './presence'
|
||||||
@@ -24,8 +26,14 @@ const callHistory = new CallHistory()
|
|||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
waitForKeyElements('call', (element) => { callHistory.showCallHistory(element) }, false)
|
waitForKeyElements('call', (element) => { callHistory.showCallHistory(element) }, false)
|
||||||
|
|
||||||
const status = new Status()
|
const availabilityService = new AvailabilityService()
|
||||||
|
availabilityService.start()
|
||||||
|
|
||||||
|
const status = new Status(availabilityService)
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false)
|
waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false)
|
||||||
|
|
||||||
waitForKeyElements('wc-account-menu i.status-indicator', (element) => { status.watchStatus(element) }, false)
|
waitForKeyElements('wc-account-menu i.status-indicator', (element) => { status.watchStatus(element) }, false)
|
||||||
|
|
||||||
|
const availability = new Availability(availabilityService)
|
||||||
|
availability.start()
|
||||||
|
|||||||
+30
-19
@@ -1,17 +1,22 @@
|
|||||||
import { Config } from './config';
|
|
||||||
import './status.css';
|
import './status.css';
|
||||||
import { ZcStatus } from './zc-status';
|
import { AvailabilityInfo } from './availability-info';
|
||||||
import GM_fetch from "@trim21/gm-fetch";
|
import { AvailabilityService } from './availability-service';
|
||||||
const zcIcon = require('./stopwatch-regular-full.svg');
|
const zcIcon = require('./stopwatch-regular-full.svg');
|
||||||
|
|
||||||
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
|
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
|
||||||
|
|
||||||
export class Status {
|
export class Status {
|
||||||
|
private _service: AvailabilityService;
|
||||||
private _user: string;
|
private _user: string;
|
||||||
private _enabled = false;
|
private _enabled = false;
|
||||||
private _statusOn = 'menuAvailable';
|
private _statusOn = 'menuAvailable';
|
||||||
private _statusOff = 'menuAway';
|
private _statusOff = 'menuAway';
|
||||||
private _currentStatus: boolean = undefined;
|
private _currentStatus: boolean = undefined;
|
||||||
|
private _subscribed = false;
|
||||||
|
|
||||||
|
constructor(service: AvailabilityService) {
|
||||||
|
this._service = service;
|
||||||
|
}
|
||||||
|
|
||||||
public async showStatus(element: HTMLElement) {
|
public async showStatus(element: HTMLElement) {
|
||||||
this._user = await GM.getValue('tapi-zc-user', '');
|
this._user = await GM.getValue('tapi-zc-user', '');
|
||||||
@@ -20,19 +25,29 @@ export class Status {
|
|||||||
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
|
this._statusOff = await GM.getValue('tapi-zc-off', 'menuAvailable');
|
||||||
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
|
console.log('tapi-zc-user', this._user, 'tapi-zc-enabled', this._enabled, 'tapi-zc-on', this._statusOn, 'tapi-zc-off', this._statusOff);
|
||||||
|
|
||||||
this.checkStatus();
|
this.subscribeOnce();
|
||||||
|
|
||||||
waitForKeyElements("wc-account-menu > div > ul", (element: HTMLElement) => { this.addZcStatusPopup(element) }, true);
|
waitForKeyElements("wc-account-menu > div > ul", (element: HTMLElement) => { this.addZcStatusPopup(element) }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkStatus() {
|
private subscribeOnce() {
|
||||||
if (this._enabled) {
|
if (this._subscribed) {
|
||||||
try {
|
return;
|
||||||
var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
|
}
|
||||||
if (response.status == 200) {
|
this._subscribed = true;
|
||||||
var status = await response.json() as ZcStatus;
|
this._service.subscribe((avs) => this.onAvailabilities(avs));
|
||||||
if (this._currentStatus !== status.loggedIn) {
|
}
|
||||||
this._currentStatus = status.loggedIn;
|
|
||||||
|
private onAvailabilities(avs: AvailabilityInfo[]) {
|
||||||
|
if (!this._enabled || !this._user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var entry = avs.find(a => a.user === this._user);
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._currentStatus !== entry.loggedIn) {
|
||||||
|
this._currentStatus = entry.loggedIn;
|
||||||
console.log('New status, loggedIn', this._currentStatus);
|
console.log('New status, loggedIn', this._currentStatus);
|
||||||
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
||||||
var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
|
var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
|
||||||
@@ -44,12 +59,6 @@ export class Status {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
setTimeout(() => this.checkStatus(), 30000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private addZcStatusPopup(element: HTMLElement) {
|
private addZcStatusPopup(element: HTMLElement) {
|
||||||
var divider = document.createElement('li');
|
var divider = document.createElement('li');
|
||||||
@@ -149,7 +158,9 @@ export class Status {
|
|||||||
GM.setValue('tapi-zc-enabled', this._enabled);
|
GM.setValue('tapi-zc-enabled', this._enabled);
|
||||||
console.log('tapi-zc-enabled', this._enabled);
|
console.log('tapi-zc-enabled', this._enabled);
|
||||||
this._currentStatus = undefined;
|
this._currentStatus = undefined;
|
||||||
this.checkStatus();
|
if (this._enabled) {
|
||||||
|
this.onAvailabilities(this._service.availabilities);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var zcOn = document.getElementById('tapi-zc-on') as HTMLSelectElement;
|
var zcOn = document.getElementById('tapi-zc-on') as HTMLSelectElement;
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export class ZcStatus {
|
|
||||||
public user: string;
|
|
||||||
public loggedIn: boolean;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
setlocal
|
setlocal
|
||||||
cd /d %~dp0
|
cd /d %~dp0
|
||||||
|
|
||||||
docker build -t source.cp-austria.at/cpatrd/3cx_tapi:latest -f Dockerfile ..
|
docker build -t source.cp-austria.at/cpatrd/3cx_tapi:latest -f server/src/CPATapi.Server/Dockerfile .
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: Docker build failed!
|
echo ERROR: Docker build failed!
|
||||||
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client", "src\CPATa
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client.Tests", "test\CPATapi.Client.Tests\CPATapi.Client.Tests.csproj", "{17F37791-4F68-46D5-8CF5-5F1736F6776E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client.Tests", "test\CPATapi.Client.Tests\CPATapi.Client.Tests.csproj", "{17F37791-4F68-46D5-8CF5-5F1736F6776E}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Server.Tests", "test\CPATapi.Server.Tests\CPATapi.Server.Tests.csproj", "{72486DC9-2C7D-409B-9E14-6D90F67B92CC}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -30,6 +32,10 @@ Global
|
|||||||
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
// <auto-generated/>
|
// <auto-generated/>
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
using CPATapi.Client.Availability.Item;
|
using CPATapi.Client.Availability.Item;
|
||||||
using CPATapi.Client.Availability.Users;
|
using CPATapi.Client.Models;
|
||||||
using Microsoft.Kiota.Abstractions.Extensions;
|
using Microsoft.Kiota.Abstractions.Extensions;
|
||||||
|
using Microsoft.Kiota.Abstractions.Serialization;
|
||||||
using Microsoft.Kiota.Abstractions;
|
using Microsoft.Kiota.Abstractions;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
using System;
|
using System;
|
||||||
namespace CPATapi.Client.Availability
|
namespace CPATapi.Client.Availability
|
||||||
{
|
{
|
||||||
@@ -16,11 +18,6 @@ namespace CPATapi.Client.Availability
|
|||||||
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
|
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
|
||||||
public partial class AvailabilityRequestBuilder : BaseRequestBuilder
|
public partial class AvailabilityRequestBuilder : BaseRequestBuilder
|
||||||
{
|
{
|
||||||
/// <summary>The users property</summary>
|
|
||||||
public global::CPATapi.Client.Availability.Users.UsersRequestBuilder Users
|
|
||||||
{
|
|
||||||
get => new global::CPATapi.Client.Availability.Users.UsersRequestBuilder(PathParameters, RequestAdapter);
|
|
||||||
}
|
|
||||||
/// <summary>Gets an item from the CPATapi.Client.Availability.item collection</summary>
|
/// <summary>Gets an item from the CPATapi.Client.Availability.item collection</summary>
|
||||||
/// <param name="position">Unique identifier of the item</param>
|
/// <param name="position">Unique identifier of the item</param>
|
||||||
/// <returns>A <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/></returns>
|
/// <returns>A <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/></returns>
|
||||||
@@ -49,6 +46,55 @@ namespace CPATapi.Client.Availability
|
|||||||
public AvailabilityRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability", rawUrl)
|
public AvailabilityRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability", rawUrl)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
/// <returns>A List<global::CPATapi.Client.Models.Availability></returns>
|
||||||
|
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
|
||||||
|
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||||
|
#nullable enable
|
||||||
|
public async Task<List<global::CPATapi.Client.Models.Availability>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
#nullable restore
|
||||||
|
#else
|
||||||
|
public async Task<List<global::CPATapi.Client.Models.Availability>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
var requestInfo = ToGetRequestInformation(requestConfiguration);
|
||||||
|
var collectionResult = await RequestAdapter.SendCollectionAsync<global::CPATapi.Client.Models.Availability>(requestInfo, global::CPATapi.Client.Models.Availability.CreateFromDiscriminatorValue, default, cancellationToken).ConfigureAwait(false);
|
||||||
|
return collectionResult?.AsList();
|
||||||
|
}
|
||||||
|
/// <returns>A <see cref="RequestInformation"/></returns>
|
||||||
|
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||||
|
#nullable enable
|
||||||
|
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
|
||||||
|
{
|
||||||
|
#nullable restore
|
||||||
|
#else
|
||||||
|
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
|
||||||
|
requestInfo.Configure(requestConfiguration);
|
||||||
|
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
|
||||||
|
return requestInfo;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="global::CPATapi.Client.Availability.AvailabilityRequestBuilder"/></returns>
|
||||||
|
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
|
||||||
|
public global::CPATapi.Client.Availability.AvailabilityRequestBuilder WithUrl(string rawUrl)
|
||||||
|
{
|
||||||
|
return new global::CPATapi.Client.Availability.AvailabilityRequestBuilder(rawUrl, RequestAdapter);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for the request such as headers, query parameters, and middleware options.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
|
||||||
|
public partial class AvailabilityRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ namespace CPATapi.Client.Availability.Item
|
|||||||
/// <returns>A <see cref="global::CPATapi.Client.Models.Availability"/></returns>
|
/// <returns>A <see cref="global::CPATapi.Client.Models.Availability"/></returns>
|
||||||
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
|
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
|
||||||
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||||
|
/// <exception cref="global::CPATapi.Client.Models.ProblemDetails">When receiving a 404 status code</exception>
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||||
#nullable enable
|
#nullable enable
|
||||||
public async Task<global::CPATapi.Client.Models.Availability?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
|
public async Task<global::CPATapi.Client.Models.Availability?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||||
@@ -46,7 +47,11 @@ namespace CPATapi.Client.Availability.Item
|
|||||||
{
|
{
|
||||||
#endif
|
#endif
|
||||||
var requestInfo = ToGetRequestInformation(requestConfiguration);
|
var requestInfo = ToGetRequestInformation(requestConfiguration);
|
||||||
return await RequestAdapter.SendAsync<global::CPATapi.Client.Models.Availability>(requestInfo, global::CPATapi.Client.Models.Availability.CreateFromDiscriminatorValue, default, cancellationToken).ConfigureAwait(false);
|
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
|
||||||
|
{
|
||||||
|
{ "404", global::CPATapi.Client.Models.ProblemDetails.CreateFromDiscriminatorValue },
|
||||||
|
};
|
||||||
|
return await RequestAdapter.SendAsync<global::CPATapi.Client.Models.Availability>(requestInfo, global::CPATapi.Client.Models.Availability.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
/// <returns>A <see cref="RequestInformation"/></returns>
|
/// <returns>A <see cref="RequestInformation"/></returns>
|
||||||
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<PackageId>CPATapi.Client</PackageId>
|
<PackageId>CPATapi.Client</PackageId>
|
||||||
<Authors>Daniel Triendl</Authors>
|
<Authors>Daniel Triendl</Authors>
|
||||||
<Company>CP Solutions GmbH</Company>
|
<Company>CP Solutions GmbH</Company>
|
||||||
<Version>9.4.0</Version>
|
<Version>9.6.0</Version>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ namespace CPATapi.Client.Models
|
|||||||
{
|
{
|
||||||
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
|
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
|
||||||
public IDictionary<string, object> AdditionalData { get; set; }
|
public IDictionary<string, object> AdditionalData { get; set; }
|
||||||
|
/// <summary>The extension property</summary>
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||||
|
#nullable enable
|
||||||
|
public string? Extension { get; set; }
|
||||||
|
#nullable restore
|
||||||
|
#else
|
||||||
|
public string Extension { get; set; }
|
||||||
|
#endif
|
||||||
/// <summary>The loggedIn property</summary>
|
/// <summary>The loggedIn property</summary>
|
||||||
public bool? LoggedIn { get; set; }
|
public bool? LoggedIn { get; set; }
|
||||||
/// <summary>The user property</summary>
|
/// <summary>The user property</summary>
|
||||||
@@ -49,6 +57,7 @@ namespace CPATapi.Client.Models
|
|||||||
{
|
{
|
||||||
return new Dictionary<string, Action<IParseNode>>
|
return new Dictionary<string, Action<IParseNode>>
|
||||||
{
|
{
|
||||||
|
{ "extension", n => { Extension = n.GetStringValue(); } },
|
||||||
{ "loggedIn", n => { LoggedIn = n.GetBoolValue(); } },
|
{ "loggedIn", n => { LoggedIn = n.GetBoolValue(); } },
|
||||||
{ "user", n => { User = n.GetStringValue(); } },
|
{ "user", n => { User = n.GetStringValue(); } },
|
||||||
};
|
};
|
||||||
@@ -60,6 +69,7 @@ namespace CPATapi.Client.Models
|
|||||||
public virtual void Serialize(ISerializationWriter writer)
|
public virtual void Serialize(ISerializationWriter writer)
|
||||||
{
|
{
|
||||||
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
|
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
|
||||||
|
writer.WriteStringValue("extension", Extension);
|
||||||
writer.WriteBoolValue("loggedIn", LoggedIn);
|
writer.WriteBoolValue("loggedIn", LoggedIn);
|
||||||
writer.WriteStringValue("user", User);
|
writer.WriteStringValue("user", User);
|
||||||
writer.WriteAdditionalData(AdditionalData);
|
writer.WriteAdditionalData(AdditionalData);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"descriptionHash": "1B47F98D82C5E24FB3ABEDB3BF90615424A8F19620EA2621D23B7FC69F4F7BF27D512A11098EF4D8940C7D243DEEB59C0CA038264430AD9150B612F5046C8146",
|
"descriptionHash": "3ADB4B190A2637B9EC01981B2508C539F2A582D95310D01FF97D2F2C068B9024CDC66F4D14F486265ED22314E9EEB2EA7CD3BF0F3D1ECC061BA7B9734B520A9D",
|
||||||
"descriptionLocation": "../CPATapi.Server/CPATapi.Server.json",
|
"descriptionLocation": "../CPATapi.Server/CPATapi.Server.json",
|
||||||
"lockFileVersion": "1.0.0",
|
"lockFileVersion": "1.0.0",
|
||||||
"kiotaVersion": "1.30.0",
|
"kiotaVersion": "1.30.0",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<DockerfileContext>..\..</DockerfileContext>
|
<DockerfileContext>..\..</DockerfileContext>
|
||||||
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
|
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
|
||||||
<Version>9.4.0</Version>
|
<Version>9.6.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -26,4 +26,12 @@
|
|||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="CPATapi.Server.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -11,19 +11,24 @@ namespace CPATapi.Server.Controllers;
|
|||||||
public class AvailabilityController(IZeitConsensRepository zeitConsens) : ControllerBase
|
public class AvailabilityController(IZeitConsensRepository zeitConsens) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("users")]
|
[Route("")]
|
||||||
[ProducesResponseType<IEnumerable<string>>(StatusCodes.Status200OK)]
|
[ProducesResponseType<IEnumerable<Availability>>(StatusCodes.Status200OK)]
|
||||||
public async Task<IActionResult> GetUsers()
|
public async Task<IActionResult> GetUsers()
|
||||||
{
|
{
|
||||||
return Ok(await zeitConsens.GetUsersAsync());
|
return Ok(await zeitConsens.GetUsersAvailabilityAsync(DateTime.Now.Date, DateTime.Now.AddDays(1).Date));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{user}")]
|
[Route("{user}")]
|
||||||
[ProducesResponseType<Availability>(StatusCodes.Status200OK)]
|
[ProducesResponseType<Availability>(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<IActionResult> GetAvailability(string user)
|
public async Task<IActionResult> GetAvailability(string user)
|
||||||
{
|
{
|
||||||
var stampCount = (await zeitConsens.GetStampsAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date)).Count();
|
var availability = await zeitConsens.GetUserAvailabilityAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date);
|
||||||
return Ok(new Availability { User = user, LoggedIn = stampCount % 2 != 0 });
|
if (availability == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(availability);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,22 @@ USER $APP_UID
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# This stage is used to build the userscript project
|
||||||
|
FROM node:lts-alpine AS build-userscript
|
||||||
|
WORKDIR /src
|
||||||
|
COPY client .
|
||||||
|
RUN npm ci
|
||||||
|
RUN npm run build
|
||||||
|
#RUN ls -la /src/dist
|
||||||
|
|
||||||
# This stage is used to build the service project
|
# This stage is used to build the service project
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["CPATapi.Server/CPATapi.Server.csproj", "CPATapi.Server/"]
|
COPY ["server/src/CPATapi.Server/CPATapi.Server.csproj", "server/src/CPATapi.Server/CPATapi.Server.csproj"]
|
||||||
RUN dotnet restore "CPATapi.Server/CPATapi.Server.csproj"
|
RUN dotnet restore "server/src/CPATapi.Server/CPATapi.Server.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/CPATapi.Server"
|
WORKDIR "/src/server/src/CPATapi.Server"
|
||||||
RUN dotnet build "./CPATapi.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
RUN dotnet build "./CPATapi.Server.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
# This stage is used to publish the service project to be copied to the final stage
|
# This stage is used to publish the service project to be copied to the final stage
|
||||||
@@ -25,4 +33,5 @@ RUN dotnet publish "./CPATapi.Server.csproj" -c $BUILD_CONFIGURATION -o /app/pub
|
|||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
|
COPY --from=build-userscript ["/src/dist/3CX TAPI.prod.user.js", "./wwwroot/3CX_TAPI.user.js"]
|
||||||
ENTRYPOINT ["dotnet", "CPATapi.Server.dll"]
|
ENTRYPOINT ["dotnet", "CPATapi.Server.dll"]
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ namespace CPATapi.Server.Interfaces;
|
|||||||
|
|
||||||
public interface IZeitConsensRepository : IRepository
|
public interface IZeitConsensRepository : IRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<string>> GetUsersAsync();
|
Task<IEnumerable<Availability>> GetUsersAvailabilityAsync(DateTime from, DateTime to);
|
||||||
Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to);
|
Task<Availability?> GetUserAvailabilityAsync(string user, DateTime from, DateTime to);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CPATapi.Server.Models;
|
namespace CPATapi.Server.Models;
|
||||||
@@ -5,7 +6,13 @@ namespace CPATapi.Server.Models;
|
|||||||
public class Availability
|
public class Availability
|
||||||
{
|
{
|
||||||
[JsonPropertyName("user")]
|
[JsonPropertyName("user")]
|
||||||
public string User { get; set; }
|
public required string MA_USER_NAME { get; set; }
|
||||||
[JsonPropertyName("loggedIn")]
|
[JsonPropertyName("loggedIn")]
|
||||||
public bool LoggedIn { get; set; }
|
public bool LOGGED_IN { get; set; }
|
||||||
|
[JsonPropertyName("extension")]
|
||||||
|
public string? US_EXTENSION { get; set; }
|
||||||
|
[JsonPropertyName("lastStamp")]
|
||||||
|
public DateTime? LAST_STAMP { get; set; }
|
||||||
|
[JsonPropertyName("atCompany")]
|
||||||
|
public bool? AT_COMPANY { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace CPATapi.Server.Models;
|
|
||||||
|
|
||||||
public class Stamp
|
|
||||||
{
|
|
||||||
public int MA_NR { get; set; }
|
|
||||||
public DateTime BU_BU { get; set; }
|
|
||||||
}
|
|
||||||
@@ -30,6 +30,7 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.MapStaticAssets();
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
app.UseSwaggerUI(options =>
|
app.UseSwaggerUI(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,29 +6,45 @@ namespace CPATapi.Server.Repository;
|
|||||||
|
|
||||||
internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository
|
internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository
|
||||||
{
|
{
|
||||||
public async Task<IEnumerable<string>> GetUsersAsync()
|
private const string SelectStampsQuery = """
|
||||||
|
SELECT
|
||||||
|
ma.MA_USER_NAME
|
||||||
|
,bu.LOGGED_IN
|
||||||
|
,us.US_EXTENSION
|
||||||
|
,buLast.LAST_STAMP
|
||||||
|
,buLast.AT_COMPANY
|
||||||
|
FROM dbo.MA_DATEN ma
|
||||||
|
INNER JOIN projectmanagement.dbo.CP_USER us ON us.US_LOGINNAME = ma.MA_USER_NAME
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT COUNT(*) % 2 AS LOGGED_IN
|
||||||
|
FROM dbo.BU bu
|
||||||
|
WHERE bu.BU_MA_NR = ma.MA_NR
|
||||||
|
AND bu.BU_BU >= @from AND bu.BU_BU < @to
|
||||||
|
) bu
|
||||||
|
OUTER APPLY (
|
||||||
|
SELECT TOP 1
|
||||||
|
bu.BU_BU AS LAST_STAMP
|
||||||
|
,CASE WHEN bu.BU_TERM = 'Zeiterfassung' THEN 1 ELSE 0 END AS AT_COMPANY
|
||||||
|
FROM dbo.BU bu
|
||||||
|
WHERE bu.BU_MA_NR = ma.MA_NR
|
||||||
|
ORDER BY bu.BU_BU DESC
|
||||||
|
) buLast
|
||||||
|
WHERE
|
||||||
|
ma.MA_USER_AKTIV = 1
|
||||||
|
""";
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Availability>> GetUsersAvailabilityAsync(DateTime from, DateTime to)
|
||||||
{
|
{
|
||||||
await using var con = await OpenAsync("ZeitConsens");
|
await using var con = await OpenAsync("ZeitConsens");
|
||||||
return await con.QueryAsync<string>("""
|
return await con.QueryAsync<Availability>(SelectStampsQuery, new { from, to });
|
||||||
SELECT DISTINCT MA_USER_NAME
|
|
||||||
FROM dbo.MA_DATEN
|
|
||||||
WHERE MA_USER_NAME IS NOT NULL AND MA_USER_AKTIV = 1
|
|
||||||
ORDER BY MA_USER_NAME
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to)
|
public async Task<Availability?> GetUserAvailabilityAsync(string user, DateTime from, DateTime to)
|
||||||
{
|
{
|
||||||
await using var con = await OpenAsync("ZeitConsens");
|
await using var con = await OpenAsync("ZeitConsens");
|
||||||
return await con.QueryAsync<Stamp>("""
|
return await con.QueryFirstOrDefaultAsync<Availability>($"""
|
||||||
SELECT
|
{SelectStampsQuery}
|
||||||
MA_NR
|
AND ma.MA_USER_NAME = @user
|
||||||
,BU_BU
|
|
||||||
FROM dbo.BU
|
|
||||||
INNER JOIN dbo.MA_DATEN on MA_NR = BU_MA_NR
|
|
||||||
WHERE
|
|
||||||
MA_USER_NAME = @user AND
|
|
||||||
BU_BU >= @from AND BU_BU < @to
|
|
||||||
""", new { user, from, to });
|
""", new { user, from, to });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using CPATapi.Client.Models;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace CPATapi.Client.Tests;
|
namespace CPATapi.Client.Tests;
|
||||||
@@ -39,7 +40,17 @@ public class CPATapiClientTests
|
|||||||
{
|
{
|
||||||
using var scope = CreateServices();
|
using var scope = CreateServices();
|
||||||
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
|
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
|
||||||
var availability = await client.Availability["Unknown"].GetAsync();
|
Assert.ThrowsAsync<ProblemDetails>(async () => await client.Availability["Unknown"].GetAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestAvailabilityGetAll()
|
||||||
|
{
|
||||||
|
using var scope = CreateServices();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
|
||||||
|
var availability = await client.Availability.GetAsync();
|
||||||
Assert.That(availability, Is.Not.Null);
|
Assert.That(availability, Is.Not.Null);
|
||||||
|
Assert.That(availability, Is.Not.Empty);
|
||||||
|
Assert.That(availability, Has.Count.GreaterThan(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<UserSecretsId>a7b40068-a2f6-4c63-bbd9-0fd346908fb0</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\CPATapi.Server\CPATapi.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using CPATapi.Server.Repository;
|
||||||
|
|
||||||
|
namespace CPATapi.Server.Tests;
|
||||||
|
|
||||||
|
public class ZeitConsensRepositoryTest
|
||||||
|
{
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// the type specified here is just so the secrets library can
|
||||||
|
// find the UserSecretId we added in the csproj file
|
||||||
|
var builder = new ConfigurationBuilder().AddUserSecrets<ZeitConsensRepositoryTest>();
|
||||||
|
|
||||||
|
_configuration = builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IConfigurationRoot _configuration { get; set; }
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestGetUsersAvailabilityAsync()
|
||||||
|
{
|
||||||
|
var zcRepo = new ZeitConsensRepository(_configuration);
|
||||||
|
var availability= (await zcRepo.GetUsersAvailabilityAsync(DateTime.Now.Date, DateTime.Now.AddDays(1).Date)).ToList();
|
||||||
|
Assert.That(availability, Is.Not.Empty);
|
||||||
|
Assert.That(availability, Has.Count.GreaterThan(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task TestGetUserAvailabilityAsync()
|
||||||
|
{
|
||||||
|
var zcRepo = new ZeitConsensRepository(_configuration);
|
||||||
|
var availability = await zcRepo.GetUserAvailabilityAsync("CPATRD", DateTime.Now.Date, DateTime.Now.AddDays(1).Date);
|
||||||
|
Assert.That(availability, Is.Not.Null);
|
||||||
|
Assert.That(availability.MA_USER_NAME, Is.EqualTo("CPATRD"));
|
||||||
|
Assert.That(availability.US_EXTENSION, Is.EqualTo("203"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user