63 Commits

Author SHA1 Message Date
CPATRD 18c6f6f10e Updated client library 2026-04-13 10:52:35 +02:00
CPATRD 02c8e0ea3c Get Availability and extension for all users 2026-04-13 10:44:55 +02:00
CPATRD ccf1f63f1b Bump Version to 9.5.1 2026-04-10 12:06:16 +02:00
CPATRD 8459f7938d Move userscript to server 2026-04-10 12:02:03 +02:00
CPAMAP e6860461ee 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.
2026-04-10 11:42:31 +02:00
CPAMAP 16d095ca77 Claude gelernt wie man baut 2026-04-10 11:34:45 +02:00
CPAMAP fd8976fedc Claude.md für alle drinnen 2026-04-10 11:09:46 +02:00
CPATRD f890fce7c9 Bump version to 9.4.0 2026-04-09 14:14:04 +02:00
CPATRD d8b9fd7664 Added CPATapi.Server API Client 2026-04-09 14:14:04 +02:00
CPATRD 6d1d165aba Added OpenAPI 2026-04-09 14:14:04 +02:00
CPATRD 4c6342a989 Bump version to 9.3.1 2026-03-27 11:00:42 +01:00
CPATRD b47debeef2 svg loader 2026-03-27 11:00:10 +01:00
CPATRD 2886ab6522 Serilog 2026-03-19 14:32:20 +01:00
CPATRD 994cc72e1d Bump version to 9.3.0 2026-03-19 14:32:19 +01:00
CPATRD a02ce50cf3 Change favicon color depending on status 2026-03-19 14:32:19 +01:00
CPATRD c16c07ea42 New 3cx tapi server url 2026-03-19 14:32:19 +01:00
CPATRD 535cf6ea05 Removed unused imports 2026-03-19 14:32:19 +01:00
CPATRD 766ed86999 Modernize and Dockerize server 2026-03-19 14:32:14 +01:00
CPATRD 76a2bf0e88 Modernized 2026-03-19 13:42:42 +01:00
CPATRD fe8fcdf45b 3cx Tapi Server 2026-03-19 13:42:36 +01:00
CPATRD 7a99b1ab55 .gitignore aktualisiert 2026-03-18 10:59:12 +01:00
CPATRD 41f6e640e1 Packages aktualisiert 2026-03-18 10:53:51 +01:00
CPATRD 26f1902996 Moved userscript to client 2026-03-18 10:34:20 +01:00
CPAMAP c8b8199e93 No more /git - fixed wrong link 2026-03-18 08:38:08 +01:00
CPATRD 4a81cbf321 Bump version to 9.2.2 2024-10-15 17:13:21 +02:00
CPATRD b1d846de32 gm-fetch aktualisiert 2024-10-15 17:12:35 +02:00
CPATRD 748a8740eb Revert "gm-fetch für Firefox gefixt"
This reverts commit 20e011bb55.
2024-10-15 16:45:21 +02:00
CPATRD 248fbd5f0f Bump version to 9.2.0 2024-10-14 11:50:06 +02:00
CPATRD 20e011bb55 gm-fetch für Firefox gefixt 2024-10-14 11:49:10 +02:00
CPATRD bbb4a910a0 Bump version to 9.2.0 2024-10-14 11:01:38 +02:00
CPATRD 1cbde09ac6 Dep Update 2024-10-14 10:59:19 +02:00
CPATRD d107b1a49f 3cx 20 2024-10-14 09:55:09 +02:00
CPATRD 505bab6d34 Bump version to 9.1.1 2023-06-27 17:34:29 +02:00
CPATRD e1459856c2 Added line break to search results 2023-06-27 17:34:11 +02:00
CPATRD 72e7a95904 Bump version to 9.1.0 2023-06-27 17:25:14 +02:00
CPATRD 69e5857963 Fix tapi search results 2023-06-27 17:23:37 +02:00
CPATRD 9a0c476bc5 Packages aktualisiert 2023-06-27 16:42:05 +02:00
CPATRD e563279faf Bump version to 9.0.3 2022-10-03 09:52:08 +02:00
CPATRD 856181f530 Search window wurde nicht immer hinzugefügt 2022-10-03 09:50:41 +02:00
CPATRD ae0c125a50 Packages aktualisiert 2022-10-03 09:34:36 +02:00
CPATRD ba5a5c627b Bump version to 9.0.2 2022-03-31 12:21:33 +02:00
CPATRD 3bf1baeca8 Fixed search result style 2022-03-31 12:20:31 +02:00
CPATRD 6186b14b16 If no search results, dialog value entered in search field 2022-02-07 12:47:51 +01:00
CPATRD f578bd2fe1 Bump version to 9.0.0 2021-11-30 14:39:28 +01:00
CPATRD 933b445ed6 Fix tapi search 2021-11-30 14:34:29 +01:00
CPATRD bbe20d6351 Fix call notification 2021-11-30 14:20:13 +01:00
CPATRD 9e5d93bad2 Fix call history 2021-11-30 14:11:34 +01:00
CPATRD b83cef625a Fixed ZC status change 2021-11-30 13:40:10 +01:00
CPATRD ad5c8ece12 Bump Version to 8.0.1 2021-08-30 08:03:49 +02:00
CPATRD 0455cb1926 Fixed typo 2021-08-28 17:16:36 +02:00
CPATRD 3e33155276 Make popup background non-transparent 2021-08-28 11:49:49 +02:00
CPATRD de34a6c66e Updated source url 2021-08-27 18:40:33 +02:00
CPATRD a4a346b48d Version 8.0.0. 2021-08-27 18:29:36 +02:00
CPATRD cd303869c8 Status mit ZeitConsens synchronisieren 2021-08-27 18:14:09 +02:00
CPATRD 4283ee6b5c Userscript template aktualisiert 2021-08-27 18:12:26 +02:00
Daniel Triendl fec9885a64 Timemanger immer anzeigen, TAPI Suche nur wenn mehr als 3 Ziffern bei Nummer 2020-12-09 10:54:13 +01:00
Daniel Triendl 29fc426161 Medium anzeigen 2020-11-09 08:53:39 +01:00
Daniel Triendl 231d24b26a Search result submit selected first 2020-11-05 08:58:51 +01:00
Daniel Triendl f3693162ab Time Manager Link title 2020-11-05 08:58:20 +01:00
Daniel Triendl c09bdd856b Fix readme 2020-11-04 23:36:32 +01:00
Daniel Triendl ecb9097f5f Improve date parsing 2020-11-04 23:34:03 +01:00
Daniel Triendl 7db79afca2 Switch to Typescript and webpack 2020-11-04 22:59:32 +01:00
Daniel Triendl e40a0810ff Fixed display for cached callerIds 2020-11-04 16:14:41 +01:00
77 changed files with 15641 additions and 294 deletions
+9
View File
@@ -0,0 +1,9 @@
module.exports = function (api) {
api.cache(true)
const presets = ['@babel/preset-env']
return {
presets,
}
}
+3
View File
@@ -0,0 +1,3 @@
> 1%
not IE 11
not dead
+3
View File
@@ -0,0 +1,3 @@
*Dockerfile*
*docker-compose*
node_modules
+15
View File
@@ -0,0 +1,15 @@
[*]
charset=utf-8
end_of_line=lf
trim_trailing_whitespace=true
insert_final_newline=true
indent_style=space
indent_size=4
[{.babelrc,.stylelintrc,.eslintrc,jest.config,*.bowerrc,*.jsb3,*.jsb2,*.json,*.yaml,*.yml}]
indent_style=space
indent_size=2
[{*.js,*.vue,*.ts}]
indent_style=space
indent_size=2
+28
View File
@@ -0,0 +1,28 @@
---
extends:
- standard
- plugin:import/recommended
- plugin:import/typescript # this line does the trick
# or configure manually:
plugins:
- import
- promise
- standard
- '@typescript-eslint'
parser: '@typescript-eslint/parser'
parserOptions:
sourceType: module
ecmaVersion: 6
env:
browser: true
node: true
rules:
comma-dangle: off
"import/order": "error"
"no-unused-vars": "off"
"@typescript-eslint/no-unused-vars": ["error"]
+25
View File
@@ -0,0 +1,25 @@
.idea
.vscode/*
client/node_modules
client/dist
.vs/
.user
.kiota.log
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
+5031 -260
View File
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
3CX TAPI is a dual-component integration that bridges the 3CX VoIP telephony web client with a CPA project management suite (CP Solutions) and a time tracking system (ZeitConsens). It is deployed as a Tampermonkey UserScript (client) communicating with a self-hosted ASP.NET Core API (server).
## Repository Structure
```
/
├── client/ # TypeScript UserScript (Tampermonkey)
├── server/ # C# ASP.NET Core 10.0 REST API
└── 3CX_TAPI.user.js # Built distribution file (committed artifact)
```
## Client Commands
All commands run from `client/`:
```bash
npm run build # Production webpack build → dist/3CX TAPI.prod.user.js
npm run dev # Development build with live reload
npm run lint # ESLint on TypeScript/JavaScript sources
npm run anylize # Build with webpack-bundle-analyzer
```
## Versioning & Release
**Never edit these files manually — they are auto-generated:**
- `client/package-lock.json` (generated by `npm install`)
- `3CX_TAPI.user.js` (generated by build)
To release a new version:
1. Bump `version` in `client/package.json` (1st digit = major, 2nd = feature, 3rd = bugfix/minor)
2. Build: `npm run build` (from `client/`)
3. Copy: `cp "dist/3CX TAPI.prod.user.js" ../3CX_TAPI.user.js`
## Server Commands
All commands run from `server/src/CPATapi.Server/`:
```bash
dotnet build # Debug build
dotnet build -c Release # Release build
dotnet publish -c Release -o /app/publish # Publish for deployment
docker build -f Dockerfile -t cpatapi-server . # Docker image
```
No automated tests exist — only manual testing and client-side linting.
## Architecture
### Client (`client/src/`)
A Tampermonkey UserScript injected into the 3CX web UI at `https://192.168.0.154:5001/*` and `https://cpsolution.my3cx.at:5001/*`. Entry point: `src/index.js`.
Four main modules:
- **`search.ts`** — Autocomplete contact lookup box injected near the call button. Debounces input and calls `GET /search?query=...`. On selection, dials the contact via 3CX.
- **`call-history.ts`** — Monitors 3CX call history entries, does reverse lookup via `GET /callerid/{number}`, and adds a "PM Zeitbuchung" button that opens the Domizil PM time booking system with pre-filled metadata (contact ID, timestamp, duration).
- **`call-notification.ts`** — Enriches incoming call GM notifications with contact name/medium fetched from TAPI directory.
- **`status.ts`** — Polls `GET /availability/{user}` every 30 seconds and auto-syncs 3CX presence status with ZeitConsens login state (odd stamp count = logged in, even = logged out). User settings (username, enabled, status IDs) persist via `GM.setValue`.
API base URL is configured in `src/config.ts` (`Config.tapi_server_url`).
UserScript metadata (match URLs, GM grants, CORS) is in `config/metadata.cjs`.
### Server (`server/src/CPATapi.Server/`)
ASP.NET Core 10.0 Web API using Dapper for SQL Server queries. Deployed as a Docker Linux container exposing port 8080.
**API Endpoints:**
| Endpoint | Description |
|---|---|
| `GET /search?query=` | Splits query into terms, searches `CP_TAPI_DIRECTORY` by name or number (top 10) |
| `GET /contact` | Returns all contacts from `CP_TAPI_DIRECTORY` |
| `GET /callerid/{number}` | Reverse lookup: extracts last 5 digits, returns first matching contact or 404 |
| `GET /availability/users` | Lists active users from `MA_DATEN` |
| `GET /availability/{user}` | Counts today's stamps in `BU` table; odd count = logged in |
**Data flow:** Controllers → Repository interfaces → Dapper repositories → SQL Server.
Two connection strings in `appsettings.json`:
- `"Tapi"` — contacts database (`CP_TAPI_DIRECTORY` table)
- `"ZeitConsens"` — time tracking database (`MA_DATEN`, `BU` tables)
Logging uses Serilog with client IP, correlation ID, and user-agent enrichment.
View File
+28
View File
@@ -0,0 +1,28 @@
const pkg = require('../package.json')
module.exports = {
name: '3CX TAPI',
namespace: 'http://cp-solutions.at',
version: pkg.version,
author: pkg.author,
copyright: 'Copyright CP Solutions GmbH',
source: pkg.repository.url,
downloadURL: 'https://3cxtapi.cp-austria.at/3CX_TAPI.user.js',
match: [
'https://192.168.0.154:5001/*',
'https://cpsolution.my3cx.at:5001/*'
],
require: [
'https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js',
],
grant: [
'GM.xmlHttpRequest',
'GM.notification',
'GM.getValue',
'GM.setValue'
],
connect: [
'3cxtapi.cp-austria.at'
],
'run-at': 'document-end'
}
+55
View File
@@ -0,0 +1,55 @@
const path = require('path')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const webpackConfig = {
resolve: {
extensions: ['.js', '.ts']
},
optimization: {
minimize: false,
moduleIds: 'named',
},
entry: './src/index.js',
output: {
path: path.resolve(__dirname, '../dist')
},
externals: {
jquery: '$',
},
module: {
rules: [
{
use: {
loader: 'babel-loader',
},
test: /\.js$/,
},
{
test: /\.ts$/,
loader: 'ts-loader'
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader', // 将 Less 编译为 CSS
]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
]
},
{
test: /\.svg$/,
loader: 'svg-inline-loader'
}
]
},
plugins: process.env.npm_config_report ? [new BundleAnalyzerPlugin()] : [],
}
module.exports = webpackConfig
+37
View File
@@ -0,0 +1,37 @@
const path = require('path')
const { merge } = require('webpack-merge')
const LiveReloadPlugin = require('webpack-livereload-plugin')
const { UserScriptMetaDataPlugin } = require('userscript-metadata-webpack-plugin')
const metadata = require('./metadata.cjs')
const webpackConfig = require('./webpack.config.base.cjs')
metadata.require.push(
'file://' + path.resolve(__dirname, '../dist/index.prod.user.js')
)
const cfg = merge(webpackConfig, {
entry: {
prod: webpackConfig.entry,
dev: path.resolve(__dirname, './empty.cjs'),
},
output: {
filename: 'index.[name].user.js',
path: path.resolve(__dirname, '../dist'),
},
devtool: 'inline-source-map',
watch: true,
watchOptions: {
ignored: /node_modules/,
},
plugins: [
new LiveReloadPlugin({
delay: 500,
}),
new UserScriptMetaDataPlugin({
metadata,
}),
],
})
module.exports = cfg
@@ -0,0 +1,19 @@
const { merge } = require('webpack-merge')
const { UserScriptMetaDataPlugin } = require('userscript-metadata-webpack-plugin')
const metadata = require('./metadata.cjs')
const webpackConfig = require('./webpack.config.base.cjs')
const cfg = merge(webpackConfig, {
mode: 'production',
output: {
filename: metadata.name + '.prod.user.js',
},
plugins: [
new UserScriptMetaDataPlugin({
metadata,
}),
],
})
module.exports = cfg
+7607
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
{
"name": "3cx-tapi",
"description": "3CX CP Tapi and Projectmanager integration",
"version": "9.5.1",
"author": {
"name": "Daniel Triendl",
"email": "d.triendl@cp-solutions.at"
},
"eslintIgnore": [
"dist/*.js",
"node_modules"
],
"scripts": {
"lint": "eslint --ext .ts,.js src",
"preversion": "npm run lint",
"postversion": "git push --follow-tags",
"anylize": "npm_config_report=true npm run build",
"build": "webpack --mode production --config config/webpack.config.production.cjs",
"dev": "webpack --mode development --config config/webpack.config.dev.cjs"
},
"repository": {
"type": "git",
"url": "https://source.cp-austria.at/CPATRD/3cx_tapi.git"
},
"private": true,
"dependencies": {
"@trim21/gm-fetch": "^0.3.0",
"chrono-node": "^2.9.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@types/greasemonkey": "^4.0.7",
"@typescript-eslint/eslint-plugin": "^8.57.1",
"@typescript-eslint/parser": "^8.57.1",
"babel-loader": "^10.1.1",
"browserslist": "^4.21.9",
"css-loader": "^7.1.4",
"eslint": "^9.39.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "^7.2.1",
"less": "^4.6.4",
"less-loader": "^12.3.2",
"style-loader": "^4.0.0",
"svg-inline-loader": "^0.8.2",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
"userscript-metadata-webpack-plugin": "^0.4.2",
"webpack": "^5.105.4",
"webpack-bundle-analyzer": "^5.2.0",
"webpack-cli": "^7.0.2",
"webpack-livereload-plugin": "3.0.2",
"webpack-merge": "^6.0.1"
}
}
+49
View File
@@ -0,0 +1,49 @@
# 3CX TAPI
Inject CPA TAPI functions into the 3CX Webclient
## dev
1. Allow Tampermonkey's access to local file URIs [tampermonkey/faq](https://tampermonkey.net/faq.php?ext=dhdg#Q204)
2. install deps with `npm i` or `npm ci`.
3. `npm run dev` to start your development.
4. open `webpack-userscript-template/dist/index.dev.user.js` in your Chrome and install it with your userscript manager.
this userscript's meta contains `// @require file://path/to/dist/index.prod.user.js`,
it will run the code in `index.prod.user.js`,
which take [src/js/index.js](./src/js/index.js) as entry point.
every times you edit your metadata, you'll have to install it again,
because Tampermonkey don't read it from dist every times.
5. edit [src/js/index.js](./src/js/index.js) with es6, you can even import css or less files. You can use scss if you like.
livereload is default enabled, use [this chrome extension](https://chrome.google.com/webstore/detail/jnihajbhpnppcggbcgedagnkighmdlei)
## dependencies
There are two ways to using a package on npm.
### UserScript way
Like original UserScript way, you will need to add them to your [user script metadata's require section](./config/metadata.js#L13-L17) , and exclude them in [config/webpack.config.base.js](./config/webpack.config.base.js#L21-L25)
### Webpack way
just install a package and import it in your js file. webpack will pack them with in your final production js file.
## build
```
npm run build
```
`dist/3CX TAPI.prod.user.js` is the final script.
## distribution
Userscript is included in server docker image
## see also
Based on [webpack-userscript-template](https://github.com/Trim21/webpack-userscript-template/)
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Pro 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.--><path d="M432 144L432 415.5C432 415.8 432 416.2 432 416.5L432 448C432 474.5 410.5 496 384 496L192 496C165.5 496 144 474.5 144 448L144 144L432 144zM480 448L480 430.8L577 382.3C596 372.8 608 353.4 608 332.2L608 216C608 185.1 582.9 160 552 160L480 160L480 144C480 117.5 458.5 96 432 96L144 96C117.5 96 96 117.5 96 144L96 448C96 501 139 544 192 544L384 544C437 544 480 501 480 448zM555.6 339.4L480 377.2L480 208L552 208C556.4 208 560 211.6 560 216L560 332.2C560 335.2 558.3 338 555.6 339.4zM212 192C201 192 192 201 192 212L192 428C192 439 201 448 212 448C223 448 232 439 232 428L232 212C232 201 223 192 212 192zM288 192C277 192 268 201 268 212L268 428C268 439 277 448 288 448C299 448 308 439 308 428L308 212C308 201 299 192 288 192zM384 212C384 201 375 192 364 192C353 192 344 201 344 212L344 428C344 439 353 448 364 448C375 448 384 439 384 428L384 212z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Pro 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.--><path d="M256 120L256 160L384 160L384 120C384 115.6 380.4 112 376 112L264 112C259.6 112 256 115.6 256 120zM208 160L208 120C208 89.1 233.1 64 264 64L376 64C406.9 64 432 89.1 432 120L432 160L512 160C547.3 160 576 188.7 576 224L576 480C576 515.3 547.3 544 512 544L128 544C92.7 544 64 515.3 64 480L64 224C64 188.7 92.7 160 128 160L208 160zM112 368L112 480C112 488.8 119.2 496 128 496L512 496C520.8 496 528 488.8 528 480L528 368L384 368L384 384C384 401.7 369.7 416 352 416L288 416C270.3 416 256 401.7 256 384L256 368L112 368zM256 320L528 320L528 224C528 215.2 520.8 208 512 208L128 208C119.2 208 112 215.2 112 224L112 320L256 320z"/></svg>

After

Width:  |  Height:  |  Size: 862 B

+90
View File
@@ -0,0 +1,90 @@
import * as chrono from 'chrono-node'
import { TapiContact } from './tapi-contact'
import { extractNumber } from './utils'
import GM_fetch from '@trim21/gm-fetch'
import { Config } from './config'
const telephoneIcon = require('./telephone.svg');
export class CallHistory {
private callerIds: { [number: string]: TapiContact } = {}
private updateCallHistoryEntry (call: HTMLElement, callerId: TapiContact) {
var span = call.querySelector(':scope > span')
this.showTimeManager(call, call.querySelector('.date').textContent, callerId)
if (callerId && callerId.tD_NAME !== '') {
var text = span.textContent
span.textContent = callerId.tD_NAME + ' ' + callerId.tD_NUMBER
}
}
private showTimeManager (call: HTMLElement, date: string, callerId: TapiContact) {
var dateParts = date.match(/^(?<date>.*), (?<duration>[0-9]{2}:[0-9]{2}:[0-9]{2})$/)
var duration = '00:00:00'
if (dateParts) {
date = dateParts.groups.date
duration = dateParts.groups.duration
}
var parsedDate = chrono.parseDate(date)
var parsedDateDe = chrono.de.parseDate(date)
if (parsedDateDe) {
parsedDate = parsedDateDe
}
if (!parsedDate) {
return
}
// Date parsing is awful, just assume the first number is the day of month
var day = date.match(/[0-9]+/)[0]
var parsedDuration = chrono.parseDate(duration)
console.log('TAPI call history time:', date, 'parsedDate:', parsedDate, 'duration:', duration, 'parsedDuration:', parsedDuration)
var connect = parsedDate.getFullYear().toString() +
(parsedDate.getMonth() + 1).toString().padStart(2, '0') + // (January gives 0)
day.toString().padStart(2, '0') +
parsedDate.getHours().toString().padStart(2, '0') +
parsedDate.getMinutes().toString().padStart(2, '0')
var length = (parsedDuration.getHours() * 60 + parsedDuration.getMinutes()).toString()
var toolbar = call.querySelector('call-history-options')
var href = 'domizil://PM/Zeitbuchung?'
if (callerId && callerId.tD_ID) {
href += 'KontaktId=' + callerId.tD_ID + '&'
}
href += 'connect=' + connect + '&length=' + length
var a = document.createElement('a')
a.title = 'PM Zeitbuchung'
a.dataset.domizilLink = href
a.onclick = () => {
window.open(href)
}
a.innerHTML = telephoneIcon;
a.classList.add('btn');
a.classList.add('btn-plain');
toolbar.insertBefore(a, toolbar.firstChild)
}
public async showCallHistory (element: HTMLElement) {
var span = element.querySelector(':scope > span')
var number = extractNumber(span.textContent)
if (!number) {
this.updateCallHistoryEntry(element, undefined)
return
}
if (this.callerIds[number] !== undefined) {
this.updateCallHistoryEntry(element, this.callerIds[number])
} else {
var response = await GM_fetch(Config.tapi_server_url + '/callerid/' + encodeURIComponent(number))
var callerId: TapiContact = { tD_NAME: '' }
if (response.status === 200) {
callerId = await response.json() as TapiContact
}
console.log('TAPI call histroy callerid response', number, response, callerId)
this.callerIds[number] = callerId
this.updateCallHistoryEntry(element, callerId)
}
}
}
+33
View File
@@ -0,0 +1,33 @@
import GM_fetch from '@trim21/gm-fetch'
import { TapiContact } from './tapi-contact'
import { extractNumber } from './utils'
import { Config } from './config'
export class CallNotification {
public async showCallNotification (element: HTMLElement) {
var number = element.querySelector('.callNumber').textContent
console.log('TAPI call notification', number)
number = extractNumber(number)
if (!number) {
console.log('TAPI callerid no number found')
return
}
console.log('TAPI searching callerid for', number)
var response = await GM_fetch(Config.tapi_server_url + '/callerid/' + encodeURIComponent(number))
console.log('TAPI callerid response', response)
var notification = {
text: number
}
if (response.status === 200) {
var callerId = await response.json() as TapiContact
if (callerId) {
notification.text = callerId.tD_NAME + '\r\n' + number + ' (' + callerId.tD_MEDIUM + ')'
}
}
// eslint-disable-next-line no-undef
GM.notification(notification.text, 'TAPI Anruf')
}
}
+5
View File
@@ -0,0 +1,5 @@
class _Config {
public tapi_server_url: string = 'https://3cxtapi.cp-austria.at'
}
export const Config = new _Config()
+13
View File
@@ -0,0 +1,13 @@
export function debounce (func, wait) {
let timeout
return function executedFunction (...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Pro 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.--><path d="M200 0C186.7 0 176 10.7 176 24C176 45.7 184.6 66.6 200 81.9L230.1 112C236.5 118.4 240 127 240 136C240 149.3 250.7 160 264 160C277.3 160 288 149.3 288 136C288 114.3 279.4 93.4 264 78.1L233.9 48C227.5 41.6 224 33 224 24C224 10.7 213.3 0 200 0zM145.1 256L495 256C486.3 328.1 424.8 384 350.3 384L289.8 384C215.3 384 153.9 328.1 145.1 256zM126.3 208C109.6 208 96 221.6 96 238.3C96 319 145.4 388.2 215.6 417.3L202.1 448.8C198.8 448.3 195.4 448 192 448C156.7 448 128 476.7 128 512C128 547.3 156.7 576 192 576C227.3 576 256 547.3 256 512L412.7 512L433.9 561.5C439.1 573.7 453.2 579.3 465.4 574.1C477.6 568.9 483.2 554.8 478 542.6L424.4 417.3C494.6 388.2 544 319 544 238.3C544 221.6 530.4 208 513.7 208L126.3 208zM392.2 464L247.8 464L262.3 430.1C271.2 431.4 280.4 432 289.7 432L350.2 432C359.5 432 368.6 431.3 377.6 430.1L392.2 464zM192 488C205.3 488 216 498.7 216 512C216 525.3 205.3 536 192 536C178.7 536 168 525.3 168 512C168 498.7 178.7 488 192 488zM336 24C336 10.7 325.3 0 312 0C298.7 0 288 10.7 288 24C288 45.7 296.6 66.6 312 81.9L342.1 112C348.5 118.4 352 127 352 136C352 149.3 362.7 160 376 160C389.3 160 400 149.3 400 136C400 114.3 391.4 93.4 376 78.1L345.9 48C339.5 41.6 336 33 336 24z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M144 288C144 190.8 222.8 112 320 112C417.2 112 496 190.8 496 288L496 332.8C481.9 324.6 465.5 320 448 320L432 320C405.5 320 384 341.5 384 368L384 496C384 522.5 405.5 544 432 544L448 544C501 544 544 501 544 448L544 288C544 164.3 443.7 64 320 64C196.3 64 96 164.3 96 288L96 448C96 501 139 544 192 544L208 544C234.5 544 256 522.5 256 496L256 368C256 341.5 234.5 320 208 320L192 320C174.5 320 158.1 324.7 144 332.8L144 288zM144 416C144 389.5 165.5 368 192 368L208 368L208 496L192 496C165.5 496 144 474.5 144 448L144 416zM496 416L496 448C496 474.5 474.5 496 448 496L432 496L432 368L448 368C474.5 368 496 389.5 496 416z"/></svg>

After

Width:  |  Height:  |  Size: 843 B

+31
View File
@@ -0,0 +1,31 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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'
console.log('script start')
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)
const callHistory = new CallHistory()
// eslint-disable-next-line no-undef
waitForKeyElements('call', (element) => { callHistory.showCallHistory(element) }, false)
const status = new Status()
// eslint-disable-next-line no-undef
waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false)
waitForKeyElements('wc-account-menu i.status-indicator', (element) => { status.watchStatus(element) }, false)
+27
View File
@@ -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;
}
+81
View File
@@ -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<void> {
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()
}
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Pro 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.--><path d="M544 272C555.1 272 565.9 273.6 576 276.7L576 64L64 64L64 276.7C74.1 273.7 84.9 272 96 272C101.4 272 106.8 272.4 112 273.1L112 112L528 112L528 273.1C533.2 272.4 538.6 272 544 272zM160 384C160 348.7 131.3 320 96 320C60.7 320 32 348.7 32 384C32 419.3 60.7 448 96 448C131.3 448 160 419.3 160 384zM160 480L32 480L0 576L192 576L160 480zM384 384C384 348.7 355.3 320 320 320C284.7 320 256 348.7 256 384C256 419.3 284.7 448 320 448C355.3 448 384 419.3 384 384zM384 480L256 480L224 576L416 576L384 480zM544 448C579.3 448 608 419.3 608 384C608 348.7 579.3 320 544 320C508.7 320 480 348.7 480 384C480 419.3 508.7 448 544 448zM640 576L608 480L480 480L448 576L640 576z"/></svg>

After

Width:  |  Height:  |  Size: 900 B

+48
View File
@@ -0,0 +1,48 @@
.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;
}
.tapi-search-autocomplete input {
border: 1px solid transparent;
background-color: #f1f1f1;
/*padding: 10px;*/
/*font-size: 16px;*/
}
.tapi-search-autocomplete input[type=text] {
background-color: #f1f1f1;
width: 100%;
}
.tapi-search-autocomplete-items {
position: absolute;
border: 1px solid #d4d4d4;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}
.tapi-search-autocomplete-items div {
padding: 10px;
cursor: pointer;
background-color: #fff;
border-bottom: 1px solid #d4d4d4;
color: #000;
}
.tapi-search-autocomplete-items div p {
margin: 0;
}
.tapi-search-autocomplete-items div:hover, .tapi-search-autocomplete-active {
/*when hovering an item:*/
background-color: #E7E6E6 !important;
}
+156
View File
@@ -0,0 +1,156 @@
import './search.css'
import { TapiContact } from './tapi-contact'
import { debounce } from './debounce'
import { fireChangeEvents } from './utils'
import GM_fetch from '@trim21/gm-fetch'
import { Config } from './config'
export class Search {
private currentSearchText = ''
public createSearchWindow (element: HTMLElement) {
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) {
items = document.getElementsByClassName('tapi-search-autocomplete-item')
}
if (items.length > 0) {
this.dial((<HTMLElement>items[0]).dataset.tapiNumber)
} else {
this.dial((<HTMLInputElement>document.getElementById('tapiSearchInput')).value)
}
return false
}
var searchBox = document.createElement('div')
searchBox.classList.add('tapi-search-autocomplete')
searchBox.style.width = '200px'
searchBox.id = 'tapiSearchBox'
form.appendChild(searchBox)
var search = document.createElement('input')
search.id = 'tapiSearchInput'
search.autocomplete = 'off'
search.placeholder = 'TAPI Suche'
search.onfocus = () => { this.doSearch() }
search.onkeydown = (ev) => { this.doSearchKeyDown(ev) }
search.onblur = () => {
console.log('TAPI Search exit', this)
setTimeout(() => {
console.log('TAPI clear search results')
this.removeSearchResults()
}, 250)
}
searchBox.appendChild(search)
element.parentElement.insertBefore(form, element)
}
private removeSearchResults () {
var resultList = document.getElementById('tapi-search-autocomplete-list')
if (resultList) {
resultList.parentNode.removeChild(resultList)
}
this.currentSearchText = ''
}
private doSearchKeyDown (ev: KeyboardEvent) {
if (ev.key === 'ArrowUp') {
let items = document.getElementsByClassName('tapi-search-autocomplete-active')
if (items.length > 0) {
var prev = <Element>items[0].previousSibling
}
if (!prev) {
items = document.getElementsByClassName('tapi-search-autocomplete-item')
if (items.length > 0) {
prev = items[items.length - 1]
}
}
if (prev) {
this.selectResult(prev)
prev.scrollIntoView(true)
}
} else if (ev.key === 'ArrowDown') {
let items = document.getElementsByClassName('tapi-search-autocomplete-active')
if (items.length > 0) {
var next = <Element>items[0].nextSibling
}
if (!next) {
items = document.getElementsByClassName('tapi-search-autocomplete-item')
if (items.length > 0) {
next = items[0]
}
}
if (next) {
this.selectResult(next)
next.scrollIntoView(false)
}
} else {
this.doSearch()
}
}
private doSearch = debounce(async () => {
var search = <HTMLInputElement>document.getElementById('tapiSearchInput')
var searchText = search.value.trim()
if (searchText === '') {
this.removeSearchResults()
return
} else if (searchText === this.currentSearchText) {
return
}
console.log('Searching TAPI')
var response = await GM_fetch(Config.tapi_server_url + '/search?query=' + encodeURIComponent(searchText))
console.log('TAPI Search response', response)
var contacts = await response.json() as TapiContact[]
console.log('TAPI Contacts', contacts)
this.removeSearchResults()
this.currentSearchText = searchText
var results = document.createElement('div');
results.setAttribute('id', 'tapi-search-autocomplete-list')
results.setAttribute('class', 'tapi-search-autocomplete-items')
document.getElementById('tapiSearchBox').appendChild(results)
contacts.forEach(contact => {
var item = document.createElement('div');
item.setAttribute('class', 'tapi-search-autocomplete-item')
var p = document.createElement('p')
p.innerHTML = contact.tD_NAME + '<br>' + contact.tD_MEDIUM + ': ' + contact.tD_NUMBER_TAPI
item.appendChild(p)
item.onclick = () => { this.dial(contact.tD_NUMBER_TAPI) }
item.onmouseover = () => { this.selectResult(item) }
item.dataset.tapiNumber = contact.tD_NUMBER_TAPI
results.appendChild(item);
})
}, 200)
private selectResult (item: Element) {
console.log('Select item', item)
var items = document.getElementsByClassName('tapi-search-autocomplete-active')
for (var i of items) {
i.classList.remove('tapi-search-autocomplete-active')
}
item.classList.add('tapi-search-autocomplete-active')
}
private dial (number: string) {
console.log('TAPI Search dialing', number);
var searchInput = document.getElementById('dialpad-input');
(<HTMLInputElement>searchInput).value = number;
(<HTMLInputElement>searchInput).focus;
fireChangeEvents(searchInput);
var toaster = document.querySelector('toaster-container');
if (window.getComputedStyle(toaster, null).display == 'none') {
document.getElementById('menuDialer').click();
}
}
}
+19
View File
@@ -0,0 +1,19 @@
.tapi-dropdown {
position: relative;
display: inline-block;
}
.tapi-dropdown-content {
display: none;
position: absolute;
min-width: 200px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
color: #000;
background-color: #fff;
}
.show {
display: block;
}
+203
View File
@@ -0,0 +1,203 @@
import { Config } from './config';
import './status.css';
import { ZcStatus } from './zc-status';
import GM_fetch from "@trim21/gm-fetch";
const zcIcon = require('./stopwatch-regular-full.svg');
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
export class Status {
private _user: string;
private _enabled = false;
private _statusOn = 'menuAvailable';
private _statusOff = 'menuAway';
private _currentStatus: boolean = undefined;
public async showStatus(element: HTMLElement) {
this._user = await GM.getValue('tapi-zc-user', '');
this._enabled = await GM.getValue('tapi-zc-enabled', false);
this._statusOn = await GM.getValue('tapi-zc-on', '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);
this.checkStatus();
waitForKeyElements("wc-account-menu > div > ul", (element: HTMLElement) => { this.addZcStatusPopup(element) }, true);
}
private async checkStatus() {
if (this._enabled) {
try {
var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
if (response.status == 200) {
var status = await response.json() as ZcStatus;
if (this._currentStatus !== status.loggedIn) {
this._currentStatus = status.loggedIn;
console.log('New status, loggedIn', this._currentStatus);
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
avatar.click();
setTimeout(() => {
var statusId = this._currentStatus ? this._statusOn : this._statusOff;
console.log('Clicking status', statusId);
(document.getElementById(statusId) as HTMLSpanElement).click();
}, 1000);
}
}
} catch (error) {
console.log(error);
}
setTimeout(() => this.checkStatus(), 30000);
}
}
private addZcStatusPopup(element: HTMLElement) {
var divider = document.createElement('li');
divider.classList.add('divider');
divider.classList.add('dropdown-divider');
element.appendChild(divider);
var menu = document.createElement('li');
menu.role = 'menuitem';
element.appendChild(menu);
var link = document.createElement('a');
link.id = 'tapi-zc-button';
//link.innerText = 'ZeitConsens';
link.classList.add('dropdown-item');
link.classList.add('d-flex');
link.classList.add('align-items-center');
link.classList.add('gap-2');
link.onclick = () => {
document.getElementById('zc-modal').classList.toggle('show');
}
menu.appendChild(link);
var icon = document.createElement('span');
icon.classList.add('icon');
icon.classList.add('svg-xs');
icon.innerHTML = zcIcon;
link.appendChild(icon);
var text = document.createElement('span');
text.innerText = 'ZeitConsens';
link.appendChild(text);
var html =
'<div role="document" class="modal-dialog">' +
' <div class="modal-content">' +
' <div class="modal-header">' +
' <h4 class="modal-title">ZeitConsens Status</h4><button id="zc-btnClose" type="button" aria-label="Close" class="btn-close" data-qa="modal-cross"></button>' +
' </div>' +
' <div class="modal-body">' +
' <div class="form-group">' +
' <label for="tapi-zc-user">Username</label>' +
' <input type="text" class="form-control" name="tapi-zc-user" id="tapi-zc-user">' +
' </div>' +
' <div class="form-group">' +
' <label for="tapi-zc-on">Signed in</label>' +
' <select id="tapi-zc-on" class="form-control">' +
' <option value="menuAvailable">Available</option>' +
' <option value="menuOutofoffice">Do Not Disturb</option>' +
' <option value="menuCustom1">Verfügbar DW</option>' +
' </select>' +
' </div>' +
' <div class="form-group">' +
' <label for="tapi-zc-off">Signed out</label>' +
' <select id="tapi-zc-off" class="form-control">' +
' <option value="menuAway">Away</option>' +
' <option value="menuOutofoffice">Do Not Disturb</option>' +
' </select>' +
' </div>' +
' <div class="checkbox">' +
' <label class="i-checks" for="tapi-zc-enabled">' +
' <input type="checkbox" id="tapi-zc-enabled">' +
' <i></i><span>Enabled</span>' +
' </label>' +
' </div>' +
' </div>' +
//' <div class="modal-footer">' +
//' <button id="zc-btnOk" type="button" class="btn btn-primary" data-qa="modal-ok">OK </button>' +
//' <button id="zc-btnCancel" type="button" class="btn btn-border" data-qa="modal-close">Cancel </button>' +
//' </div>' +
' </div>' +
'</div>';
var modal = document.createElement('modal-container');
modal.id = 'zc-modal';
modal.classList.add('modal');
modal.classList.add('fade');
modal.innerHTML = html;
document.getElementsByTagName('body')[0].appendChild(modal);
var btnClose = document.getElementById('zc-btnClose');
btnClose.onclick = () => {
document.getElementById('zc-modal').classList.toggle('show');
}
var zcUser = document.getElementById('tapi-zc-user') as HTMLInputElement;
zcUser.value = this._user;
zcUser.onchange = () => {
this._user = zcUser.value;
GM.setValue('tapi-zc-user', this._user);
console.log('tapi-zc-user', this._user);
this._currentStatus = undefined;
}
var zcEnabled = document.getElementById('tapi-zc-enabled') as HTMLInputElement;
zcEnabled.checked = this._enabled;
zcEnabled.onchange = () => {
this._enabled = zcEnabled.checked;
GM.setValue('tapi-zc-enabled', this._enabled);
console.log('tapi-zc-enabled', this._enabled);
this._currentStatus = undefined;
this.checkStatus();
}
var zcOn = document.getElementById('tapi-zc-on') as HTMLSelectElement;
zcOn.value = this._statusOn;
zcOn.onchange = () => {
this._statusOn = zcOn.value;
GM.setValue('tapi-zc-on', this._statusOn);
console.log('tapi-zc-on', this._statusOn);
this._currentStatus = undefined;
}
var zcOff = document.getElementById('tapi-zc-off') as HTMLSelectElement;
zcOff.value = this._statusOff;
zcOff.onchange = () => {
this._statusOff = zcOff.value;
GM.setValue('tapi-zc-off', this._statusOff);
console.log('tapi-zc-off', this._statusOff);
this._currentStatus = undefined;
}
}
public watchStatus(element: HTMLElement) {
console.log('updateFavicon watching', element);
const attrObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
this.updateStatus(element);
}
});
});
attrObserver.observe(element, { attributes: true });
this.updateStatus(element);
}
private updateStatus(element: HTMLElement) {
console.log('Status changed', element.classList);
var color = window.getComputedStyle(element).getPropertyValue("background-color");
console.log('Background color:', color);
this.setFaviconColor(color);
}
private setFaviconColor(color: string) {
var link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = "data:image/svg+xml,%3Csvg width='32px' height='32px' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cpolyline points='9,0 20,0 32,16 20,32 9,32, 20,16' fill='" + encodeURIComponent(color) + "' id='MyLine' /%3E%3C/svg%3E";
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Pro 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.--><path d="M240 88C240 74.7 250.7 64 264 64L376 64C389.3 64 400 74.7 400 88C400 101.3 389.3 112 376 112L344 112L344 137.3C393.6 142.8 438.1 165 471.8 198.3L495 175C504.4 165.6 519.6 165.6 528.9 175C538.2 184.4 538.3 199.6 528.9 208.9L502.1 235.7C523.5 269.2 536 309.1 536 351.9C536 471.2 439.3 567.9 320 567.9C200.7 567.9 104 471.3 104 352C104 240.8 188 149.3 296 137.3L296 112L264 112C250.7 112 240 101.3 240 88zM320 184C227.2 184 152 259.2 152 352C152 444.8 227.2 520 320 520C412.8 520 488 444.8 488 352C488 259.2 412.8 184 320 184zM344 248L344 352C344 365.3 333.3 376 320 376C306.7 376 296 365.3 296 352L296 248C296 234.7 306.7 224 320 224C333.3 224 344 234.7 344 248z"/></svg>

After

Width:  |  Height:  |  Size: 906 B

+8
View File
@@ -0,0 +1,8 @@
/* eslint-disable camelcase */
export interface TapiContact {
tD_ID?: string;
tD_NAME: string;
tD_NUMBER?: string;
tD_NUMBER_TAPI?: string;
tD_MEDIUM?: string;
}
+12
View File
@@ -0,0 +1,12 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 559.98 559.98">
<g>
<g>
<path d="M279.99,0C125.601,0,0,125.601,0,279.99c0,154.39,125.601,279.99,279.99,279.99c154.39,0,279.99-125.601,279.99-279.99
C559.98,125.601,434.38,0,279.99,0z M279.99,498.78c-120.644,0-218.79-98.146-218.79-218.79
c0-120.638,98.146-218.79,218.79-218.79s218.79,98.152,218.79,218.79C498.78,400.634,400.634,498.78,279.99,498.78z"/>
<path d="M304.226,280.326V162.976c0-13.103-10.618-23.721-23.716-23.721c-13.102,0-23.721,10.618-23.721,23.721v124.928
c0,0.373,0.092,0.723,0.11,1.096c-0.312,6.45,1.91,12.999,6.836,17.926l88.343,88.336c9.266,9.266,24.284,9.266,33.543,0
c9.26-9.266,9.266-24.284,0-33.544L304.226,280.326z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 821 B

+29
View File
@@ -0,0 +1,29 @@
export function extractNumber (s: string) {
var match = /(\+?[0-9]{4,})/.exec(s)
if (!match) {
return undefined
}
var number = match[1]
if (number.startsWith('+')) {
number = number.replace('+', '00')
}
return number
}
export function fireChangeEvents (element: Element) {
var changeEvent = null
changeEvent = document.createEvent('HTMLEvents')
changeEvent.initEvent('input', true, true)
element.dispatchEvent(changeEvent)
console.debug('input event dispatched for element: ' + element.id)
changeEvent = document.createEvent('HTMLEvents')
changeEvent.initEvent('keyup', true, true)
element.dispatchEvent(changeEvent)
console.debug('keyup event dispatched for element: ' + element.id)
changeEvent = document.createEvent('HTMLEvents')
changeEvent.initEvent('change', true, true)
element.dispatchEvent(changeEvent)
console.debug('change event dispatched for element: ' + element.id)
}
+4
View File
@@ -0,0 +1,4 @@
export class ZcStatus {
public user: string;
public loggedIn: boolean;
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "ESNext",
"target": "ES2022",
"allowJs": true,
"moduleResolution": "node"
}
}
+19
View File
@@ -0,0 +1,19 @@
@echo off
setlocal
cd /d %~dp0
docker build -t source.cp-austria.at/cpatrd/3cx_tapi:latest -f server/src/CPATapi.Server/Dockerfile .
if errorlevel 1 (
echo.
echo ERROR: Docker build failed!
exit /b 1
)
SET /P AREYOUSURE=Publish to source.cp-austria.at? (Y/[N])
IF /I "%AREYOUSURE%" NEQ "Y" GOTO END
docker push source.cp-austria.at/cpatrd/3cx_tapi:latest
:END
endlocal
+30
View File
@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**
+46
View File
@@ -0,0 +1,46 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.5.11612.153
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Server", "src\CPATapi.Server\CPATapi.Server.csproj", "{7879A024-A074-FE67-0546-8668213BFA99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client", "src\CPATapi.Client\CPATapi.Client.csproj", "{C60C2505-6D82-4058-9392-FA3C8F57FF8A}"
ProjectSection(ProjectDependencies) = postProject
{7879A024-A074-FE67-0546-8668213BFA99} = {7879A024-A074-FE67-0546-8668213BFA99}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client.Tests", "test\CPATapi.Client.Tests\CPATapi.Client.Tests.csproj", "{17F37791-4F68-46D5-8CF5-5F1736F6776E}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7879A024-A074-FE67-0546-8668213BFA99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7879A024-A074-FE67-0546-8668213BFA99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7879A024-A074-FE67-0546-8668213BFA99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7879A024-A074-FE67-0546-8668213BFA99}.Release|Any CPU.Build.0 = Release|Any CPU
{C60C2505-6D82-4058-9392-FA3C8F57FF8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C60C2505-6D82-4058-9392-FA3C8F57FF8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C60C2505-6D82-4058-9392-FA3C8F57FF8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C60C2505-6D82-4058-9392-FA3C8F57FF8A}.Release|Any CPU.Build.0 = Release|Any CPU
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Debug|Any CPU.ActiveCfg = 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.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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E2CF5765-9038-4ED6-AC5C-1A3E569ABC6C}
EndGlobalSection
EndGlobal
@@ -0,0 +1,100 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.Availability.Item;
using CPATapi.Client.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CPATapi.Client.Availability
{
/// <summary>
/// Builds and executes requests for operations under \Availability
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class AvailabilityRequestBuilder : BaseRequestBuilder
{
/// <summary>Gets an item from the CPATapi.Client.Availability.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/></returns>
public global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder this[string position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
urlTplParams.Add("user", position);
return new global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Availability.AvailabilityRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public AvailabilityRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Availability.AvailabilityRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public AvailabilityRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability", rawUrl)
{
}
/// <returns>A List&lt;global::CPATapi.Client.Models.Availability&gt;</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
@@ -0,0 +1,91 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CPATapi.Client.Availability.Item
{
/// <summary>
/// Builds and executes requests for operations under \Availability\{user}
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class WithUserItemRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithUserItemRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability/{user}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithUserItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability/{user}", rawUrl)
{
}
/// <returns>A <see cref="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>
/// <exception cref="global::CPATapi.Client.Models.ProblemDetails">When receiving a 404 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<global::CPATapi.Client.Models.Availability?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<global::CPATapi.Client.Models.Availability> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
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>
/// <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", "application/json, 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.Item.WithUserItemRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder WithUrl(string rawUrl)
{
return new global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder(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 WithUserItemRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,86 @@
// <auto-generated/>
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CPATapi.Client.Availability.Users
{
/// <summary>
/// Builds and executes requests for operations under \Availability\users
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class UsersRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Availability.Users.UsersRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public UsersRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability/users", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Availability.Users.UsersRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public UsersRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability/users", rawUrl)
{
}
/// <returns>A List&lt;string&gt;</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<string>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<string>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var collectionResult = await RequestAdapter.SendPrimitiveCollectionAsync<string>(requestInfo, 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.Users.UsersRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::CPATapi.Client.Availability.Users.UsersRequestBuilder WithUrl(string rawUrl)
{
return new global::CPATapi.Client.Availability.Users.UsersRequestBuilder(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 UsersRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>CPATapi.Client</PackageId>
<Authors>Daniel Triendl</Authors>
<Company>CP Solutions GmbH</Company>
<Version>9.6.0</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Microsoft.Kiota.Bundle" Version="1.22.1" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="kiota generate -l CSharp -c CPATapiClient -n CPATapi.Client -d ..\CPATapi.Server\CPATapi.Server.json -o .\" />
</Target>
</Project>
@@ -0,0 +1,61 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.Availability;
using CPATapi.Client.CallerId;
using CPATapi.Client.Contact;
using CPATapi.Client.Search;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Serialization.Form;
using Microsoft.Kiota.Serialization.Json;
using Microsoft.Kiota.Serialization.Multipart;
using Microsoft.Kiota.Serialization.Text;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
namespace CPATapi.Client
{
/// <summary>
/// The main entry point of the SDK, exposes the configuration and the fluent API.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CPATapiClient : BaseRequestBuilder
{
/// <summary>The Availability property</summary>
public global::CPATapi.Client.Availability.AvailabilityRequestBuilder Availability
{
get => new global::CPATapi.Client.Availability.AvailabilityRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The CallerId property</summary>
public global::CPATapi.Client.CallerId.CallerIdRequestBuilder CallerId
{
get => new global::CPATapi.Client.CallerId.CallerIdRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The Contact property</summary>
public global::CPATapi.Client.Contact.ContactRequestBuilder Contact
{
get => new global::CPATapi.Client.Contact.ContactRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The Search property</summary>
public global::CPATapi.Client.Search.SearchRequestBuilder Search
{
get => new global::CPATapi.Client.Search.SearchRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.CPATapiClient"/> and sets the default values.
/// </summary>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CPATapiClient(IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}", new Dictionary<string, object>())
{
ApiClientBuilder.RegisterDefaultSerializer<JsonSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultSerializer<TextSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultSerializer<FormSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultSerializer<MultipartSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultDeserializer<JsonParseNodeFactory>();
ApiClientBuilder.RegisterDefaultDeserializer<TextParseNodeFactory>();
ApiClientBuilder.RegisterDefaultDeserializer<FormParseNodeFactory>();
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,48 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.CallerId.Item;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
namespace CPATapi.Client.CallerId
{
/// <summary>
/// Builds and executes requests for operations under \CallerId
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CallerIdRequestBuilder : BaseRequestBuilder
{
/// <summary>Gets an item from the CPATapi.Client.CallerId.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder"/></returns>
public global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder this[string position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
urlTplParams.Add("number", position);
return new global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.CallerId.CallerIdRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CallerIdRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/CallerId", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.CallerId.CallerIdRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CallerIdRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/CallerId", rawUrl)
{
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,91 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CPATapi.Client.CallerId.Item
{
/// <summary>
/// Builds and executes requests for operations under \CallerId\{number}
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class WithNumberItemRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithNumberItemRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/CallerId/{number}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public WithNumberItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/CallerId/{number}", rawUrl)
{
}
/// <returns>A <see cref="global::CPATapi.Client.Models.TapiContact"/></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>
/// <exception cref="global::CPATapi.Client.Models.ProblemDetails">When receiving a 404 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<global::CPATapi.Client.Models.TapiContact?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<global::CPATapi.Client.Models.TapiContact> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "404", global::CPATapi.Client.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync<global::CPATapi.Client.Models.TapiContact>(requestInfo, global::CPATapi.Client.Models.TapiContact.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <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", "application/json, 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.CallerId.Item.WithNumberItemRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder WithUrl(string rawUrl)
{
return new global::CPATapi.Client.CallerId.Item.WithNumberItemRequestBuilder(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 WithNumberItemRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,87 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CPATapi.Client.Contact
{
/// <summary>
/// Builds and executes requests for operations under \Contact
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class ContactRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Contact.ContactRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ContactRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Contact", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Contact.ContactRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ContactRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Contact", rawUrl)
{
}
/// <returns>A List&lt;global::CPATapi.Client.Models.TapiContact&gt;</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.TapiContact>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::CPATapi.Client.Models.TapiContact>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var collectionResult = await RequestAdapter.SendCollectionAsync<global::CPATapi.Client.Models.TapiContact>(requestInfo, global::CPATapi.Client.Models.TapiContact.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", "application/json");
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.Contact.ContactRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::CPATapi.Client.Contact.ContactRequestBuilder WithUrl(string rawUrl)
{
return new global::CPATapi.Client.Contact.ContactRequestBuilder(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 ContactRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,79 @@
// <auto-generated/>
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System;
namespace CPATapi.Client.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class Availability : IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
{
/// <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; }
/// <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>
public bool? LoggedIn { get; set; }
/// <summary>The user property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? User { get; set; }
#nullable restore
#else
public string User { get; set; }
#endif
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Models.Availability"/> and sets the default values.
/// </summary>
public Availability()
{
AdditionalData = new Dictionary<string, object>();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <returns>A <see cref="global::CPATapi.Client.Models.Availability"/></returns>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static global::CPATapi.Client.Models.Availability CreateFromDiscriminatorValue(IParseNode parseNode)
{
if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode));
return new global::CPATapi.Client.Models.Availability();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
/// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
{
return new Dictionary<string, Action<IParseNode>>
{
{ "extension", n => { Extension = n.GetStringValue(); } },
{ "loggedIn", n => { LoggedIn = n.GetBoolValue(); } },
{ "user", n => { User = n.GetStringValue(); } },
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer)
{
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("extension", Extension);
writer.WriteBoolValue("loggedIn", LoggedIn);
writer.WriteStringValue("user", User);
writer.WriteAdditionalData(AdditionalData);
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,108 @@
// <auto-generated/>
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System;
namespace CPATapi.Client.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class ProblemDetails : ApiException, IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
{
/// <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; }
/// <summary>The detail property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Detail { get; set; }
#nullable restore
#else
public string Detail { get; set; }
#endif
/// <summary>The instance property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Instance { get; set; }
#nullable restore
#else
public string Instance { get; set; }
#endif
/// <summary>The primary error message.</summary>
public override string Message { get => base.Message; }
/// <summary>The status property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public UntypedNode? Status { get; set; }
#nullable restore
#else
public UntypedNode Status { get; set; }
#endif
/// <summary>The title property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Title { get; set; }
#nullable restore
#else
public string Title { get; set; }
#endif
/// <summary>The type property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Type { get; set; }
#nullable restore
#else
public string Type { get; set; }
#endif
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Models.ProblemDetails"/> and sets the default values.
/// </summary>
public ProblemDetails()
{
AdditionalData = new Dictionary<string, object>();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <returns>A <see cref="global::CPATapi.Client.Models.ProblemDetails"/></returns>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static global::CPATapi.Client.Models.ProblemDetails CreateFromDiscriminatorValue(IParseNode parseNode)
{
if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode));
return new global::CPATapi.Client.Models.ProblemDetails();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
/// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
{
return new Dictionary<string, Action<IParseNode>>
{
{ "detail", n => { Detail = n.GetStringValue(); } },
{ "instance", n => { Instance = n.GetStringValue(); } },
{ "status", n => { Status = n.GetObjectValue<UntypedNode>(UntypedNode.CreateFromDiscriminatorValue); } },
{ "title", n => { Title = n.GetStringValue(); } },
{ "type", n => { Type = n.GetStringValue(); } },
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer)
{
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("detail", Detail);
writer.WriteStringValue("instance", Instance);
writer.WriteObjectValue<UntypedNode>("status", Status);
writer.WriteStringValue("title", Title);
writer.WriteStringValue("type", Type);
writer.WriteAdditionalData(AdditionalData);
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,105 @@
// <auto-generated/>
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System;
namespace CPATapi.Client.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class TapiContact : IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
{
/// <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; }
/// <summary>The tD_ID property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? TDID { get; set; }
#nullable restore
#else
public string TDID { get; set; }
#endif
/// <summary>The tD_MEDIUM property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? TDMEDIUM { get; set; }
#nullable restore
#else
public string TDMEDIUM { get; set; }
#endif
/// <summary>The tD_NAME property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? TDNAME { get; set; }
#nullable restore
#else
public string TDNAME { get; set; }
#endif
/// <summary>The tD_NUMBER property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? TDNUMBER { get; set; }
#nullable restore
#else
public string TDNUMBER { get; set; }
#endif
/// <summary>The tD_NUMBER_TAPI property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? TDNUMBERTAPI { get; set; }
#nullable restore
#else
public string TDNUMBERTAPI { get; set; }
#endif
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Models.TapiContact"/> and sets the default values.
/// </summary>
public TapiContact()
{
AdditionalData = new Dictionary<string, object>();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <returns>A <see cref="global::CPATapi.Client.Models.TapiContact"/></returns>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static global::CPATapi.Client.Models.TapiContact CreateFromDiscriminatorValue(IParseNode parseNode)
{
if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode));
return new global::CPATapi.Client.Models.TapiContact();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
/// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
{
return new Dictionary<string, Action<IParseNode>>
{
{ "tD_ID", n => { TDID = n.GetStringValue(); } },
{ "tD_MEDIUM", n => { TDMEDIUM = n.GetStringValue(); } },
{ "tD_NAME", n => { TDNAME = n.GetStringValue(); } },
{ "tD_NUMBER", n => { TDNUMBER = n.GetStringValue(); } },
{ "tD_NUMBER_TAPI", n => { TDNUMBERTAPI = n.GetStringValue(); } },
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer)
{
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("tD_ID", TDID);
writer.WriteStringValue("tD_MEDIUM", TDMEDIUM);
writer.WriteStringValue("tD_NAME", TDNAME);
writer.WriteStringValue("tD_NUMBER", TDNUMBER);
writer.WriteStringValue("tD_NUMBER_TAPI", TDNUMBERTAPI);
writer.WriteAdditionalData(AdditionalData);
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,102 @@
// <auto-generated/>
#pragma warning disable CS0618
using CPATapi.Client.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CPATapi.Client.Search
{
/// <summary>
/// Builds and executes requests for operations under \Search
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class SearchRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Search.SearchRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public SearchRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Search{?query*}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::CPATapi.Client.Search.SearchRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public SearchRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Search{?query*}", rawUrl)
{
}
/// <returns>A List&lt;global::CPATapi.Client.Models.TapiContact&gt;</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.TapiContact>?> GetAsync(Action<RequestConfiguration<global::CPATapi.Client.Search.SearchRequestBuilder.SearchRequestBuilderGetQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::CPATapi.Client.Models.TapiContact>> GetAsync(Action<RequestConfiguration<global::CPATapi.Client.Search.SearchRequestBuilder.SearchRequestBuilderGetQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var collectionResult = await RequestAdapter.SendCollectionAsync<global::CPATapi.Client.Models.TapiContact>(requestInfo, global::CPATapi.Client.Models.TapiContact.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<global::CPATapi.Client.Search.SearchRequestBuilder.SearchRequestBuilderGetQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::CPATapi.Client.Search.SearchRequestBuilder.SearchRequestBuilderGetQueryParameters>> 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.Search.SearchRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::CPATapi.Client.Search.SearchRequestBuilder WithUrl(string rawUrl)
{
return new global::CPATapi.Client.Search.SearchRequestBuilder(rawUrl, RequestAdapter);
}
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class SearchRequestBuilderGetQueryParameters
#pragma warning restore CS1591
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
[QueryParameter("query")]
public string? Query { get; set; }
#nullable restore
#else
[QueryParameter("query")]
public string Query { get; set; }
#endif
}
/// <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 SearchRequestBuilderGetRequestConfiguration : RequestConfiguration<global::CPATapi.Client.Search.SearchRequestBuilder.SearchRequestBuilderGetQueryParameters>
{
}
}
}
#pragma warning restore CS0618
@@ -0,0 +1,53 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;
namespace CPATapi.Client;
public static class ServiceExtensions
{
public static IServiceCollection AddCPATapiClient(this IServiceCollection services, Action<IServiceProvider, HttpClient> configureOptions)
{
services.AddKiotaHandlers();
services.AddHttpClient<CPATapiClientFactory>((sp, client) =>
{
configureOptions.Invoke(sp, client);
}).AttachKiotaHandlers();
services.AddTransient(sp => sp.GetRequiredService<CPATapiClientFactory>().GetClient());
return services;
}
private static IServiceCollection AddKiotaHandlers(this IServiceCollection services)
{
var handlers = KiotaClientFactory.GetDefaultHandlerActivatableTypes();
foreach (var handler in handlers)
{
if (services.All(s => s.ServiceType != handler))
{
services.AddScoped(handler);
}
}
return services;
}
private static IHttpClientBuilder AttachKiotaHandlers(this IHttpClientBuilder builder)
{
var handlers = KiotaClientFactory.GetDefaultHandlerActivatableTypes();
foreach (var handler in handlers)
{
builder.AddHttpMessageHandler((sp) => (DelegatingHandler)sp.GetRequiredService(handler));
}
return builder;
}
}
internal class CPATapiClientFactory(HttpClient httpClient)
{
private readonly HttpClient _httpClient = httpClient;
private readonly AnonymousAuthenticationProvider _authenticationProvider = new();
public CPATapiClient GetClient()
{
return new CPATapiClient(new HttpClientRequestAdapter(_authenticationProvider, httpClient: _httpClient));
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"descriptionHash": "3ADB4B190A2637B9EC01981B2508C539F2A582D95310D01FF97D2F2C068B9024CDC66F4D14F486265ED22314E9EEB2EA7CD3BF0F3D1ECC061BA7B9734B520A9D",
"descriptionLocation": "../CPATapi.Server/CPATapi.Server.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.30.0",
"clientClassName": "CPATapiClient",
"typeAccessModifier": "Public",
"clientNamespaceName": "CPATapi.Client",
"language": "CSharp",
"usesBackingStore": false,
"excludeBackwardCompatible": false,
"includeAdditionalData": true,
"disableSSLValidation": false,
"serializers": [
"Microsoft.Kiota.Serialization.Json.JsonSerializationWriterFactory",
"Microsoft.Kiota.Serialization.Text.TextSerializationWriterFactory",
"Microsoft.Kiota.Serialization.Form.FormSerializationWriterFactory",
"Microsoft.Kiota.Serialization.Multipart.MultipartSerializationWriterFactory"
],
"deserializers": [
"Microsoft.Kiota.Serialization.Json.JsonParseNodeFactory",
"Microsoft.Kiota.Serialization.Text.TextParseNodeFactory",
"Microsoft.Kiota.Serialization.Form.FormParseNodeFactory"
],
"structuredMimeTypes": [
"application/json",
"text/plain;q=0.9",
"application/x-www-form-urlencoded;q=0.2",
"multipart/form-data;q=0.1"
],
"includePatterns": [],
"excludePatterns": [],
"disabledValidationRules": []
}
+1
View File
@@ -0,0 +1 @@
CPATapi.Server.json
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>a7b40068-a2f6-4c63-bbd9-0fd346908fb0</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
<Version>9.6.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.ClientInfo" Version="2.9.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.7" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="CPATapi.Server.Tests" />
</ItemGroup>
</Project>
@@ -0,0 +1,34 @@
using CPATapi.Server.Interfaces;
using CPATapi.Server.Models;
using Microsoft.AspNetCore.Mvc;
using Dapper;
using Microsoft.Data.SqlClient;
namespace CPATapi.Server.Controllers;
[Route("[controller]")]
[ApiController]
public class AvailabilityController(IZeitConsensRepository zeitConsens) : ControllerBase
{
[HttpGet]
[Route("")]
[ProducesResponseType<IEnumerable<Availability>>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetUsers()
{
return Ok(await zeitConsens.GetUsersAvailabilityAsync(DateTime.Now.Date, DateTime.Now.AddDays(1).Date));
}
[HttpGet]
[Route("{user}")]
[ProducesResponseType<Availability>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetAvailability(string user)
{
var availability = await zeitConsens.GetUserAvailabilityAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date);
if (availability == null)
{
return NotFound();
}
return Ok(availability);
}
}
@@ -0,0 +1,30 @@
using CPATapi.Server.Interfaces;
using CPATapi.Server.Models;
using Microsoft.AspNetCore.Mvc;
namespace CPATapi.Server.Controllers;
[Route("[controller]")]
[ApiController]
public class CallerIdController(ITapiDirectoryRepository tapiDirectory) : ControllerBase
{
[HttpGet]
[Route("{number}")]
[ProducesResponseType<TapiContact>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CallerIdAsync([FromRoute] string number)
{
if (number.Length >= 5)
{
var minMatch = number[^5..];
number = "%" + minMatch;
}
var result = await tapiDirectory.SearchByNumberAsync(number);
if (result != null)
{
return Ok(result);
}
return NotFound();
}
}
@@ -0,0 +1,21 @@
using CPATapi.Server.Interfaces;
using CPATapi.Server.Models;
using Microsoft.AspNetCore.Mvc;
namespace CPATapi.Server.Controllers;
[ApiController]
[Route("[controller]")]
public class ContactController(ILogger<ContactController> logger, ITapiDirectoryRepository tapiDirectory) : ControllerBase
{
private readonly ILogger<ContactController> _logger = logger;
[HttpGet]
[Produces("application/json")]
[ProducesResponseType<IEnumerable<TapiContact>>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetAsync()
{
return Ok(await tapiDirectory.GetAllAsync());
}
}
@@ -0,0 +1,25 @@
using System.Text.RegularExpressions;
using CPATapi.Server.Interfaces;
using CPATapi.Server.Models;
using Microsoft.AspNetCore.Mvc;
namespace CPATapi.Server.Controllers;
[Route("[controller]")]
[ApiController]
public class SearchController(ITapiDirectoryRepository tapiDirectory) : ControllerBase
{
[HttpGet]
[ProducesResponseType<IEnumerable<TapiContact>>(StatusCodes.Status200OK)]
public async Task<IActionResult> SearchAsync([FromQuery] string query)
{
if (query == null)
{
return Ok(new List<TapiContact>());
}
var args = Regex.Split(query, "\\s").Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray();
return Ok(await tapiDirectory.SearchAsync(args));
}
}
+37
View File
@@ -0,0 +1,37 @@
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
# This stage is used when running from VS in fast mode (Default for Debug configuration)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
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
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["server/src/CPATapi.Server/CPATapi.Server.csproj", "server/src/CPATapi.Server/CPATapi.Server.csproj"]
RUN dotnet restore "server/src/CPATapi.Server/CPATapi.Server.csproj"
COPY . .
WORKDIR "/src/server/src/CPATapi.Server"
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
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./CPATapi.Server.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
FROM base AS final
WORKDIR /app
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"]
@@ -0,0 +1,5 @@
namespace CPATapi.Server.Interfaces;
public interface IRepository
{
}
@@ -0,0 +1,10 @@
using CPATapi.Server.Models;
namespace CPATapi.Server.Interfaces;
public interface ITapiDirectoryRepository : IRepository
{
Task<IEnumerable<TapiContact>> SearchAsync(string[] args);
Task<TapiContact?> SearchByNumberAsync(string number);
Task<IEnumerable<TapiContact>> GetAllAsync();
}
@@ -0,0 +1,9 @@
using CPATapi.Server.Models;
namespace CPATapi.Server.Interfaces;
public interface IZeitConsensRepository : IRepository
{
Task<IEnumerable<Availability>> GetUsersAvailabilityAsync(DateTime from, DateTime to);
Task<Availability?> GetUserAvailabilityAsync(string user, DateTime from, DateTime to);
}
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace CPATapi.Server.Models;
public class Availability
{
[JsonPropertyName("user")]
public required string MA_USER_NAME { get; set; }
[JsonPropertyName("loggedIn")]
public bool LOGGED_IN { get; set; }
[JsonPropertyName("extension")]
public string? US_EXTENSION { get; set; }
}
@@ -0,0 +1,10 @@
namespace CPATapi.Server.Models;
public class TapiContact
{
public string TD_ID { get; set; }
public string TD_NAME { get; set; }
public string TD_NUMBER { get; set; }
public string TD_NUMBER_TAPI { get; set; }
public string TD_MEDIUM { get; set; }
}
+44
View File
@@ -0,0 +1,44 @@
using CPATapi.Server.Interfaces;
using CPATapi.Server.Repository;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSerilog(config =>
{
config
.ReadFrom.Configuration(builder.Configuration)
.Enrich.WithClientIp()
.Enrich.WithCorrelationId()
.Enrich.WithRequestHeader("User-Agent")
.WriteTo.Console();
});
builder.Services.AddOpenApi();
builder.Services.AddTransient<ITapiDirectoryRepository, TapiDirectoryRepository>();
builder.Services.AddTransient<IZeitConsensRepository, ZeitConsensRepository>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseSerilogRequestLogging();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapStaticAssets();
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "v1");
});
app.UseAuthorization();
app.MapControllers();
await app.RunAsync();
@@ -0,0 +1,40 @@
{
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger/index.html",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"CPATapiServer": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "availability/users",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://3cxtapi.cp-austria.at:5001;https://localhost:5001;http://localhost:5000"
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/contact",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true,
"useSSL": false
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62406",
"sslPort": 0
}
}
}
@@ -0,0 +1,21 @@
using System.Data.Common;
using CPATapi.Server.Interfaces;
using Microsoft.Data.SqlClient;
namespace CPATapi.Server.Repository;
public class Repository: IRepository
{
private readonly IConfiguration _config;
protected Repository(IConfiguration config)
{
_config = config;
}
protected async Task<DbConnection> OpenAsync(string name)
{
var db = new SqlConnection(_config.GetConnectionString(name));
await db.OpenAsync();
return db;
}
}
@@ -0,0 +1,71 @@
using System.Text;
using CPATapi.Server.Interfaces;
using CPATapi.Server.Models;
using Dapper;
namespace CPATapi.Server.Repository;
internal class TapiDirectoryRepository(IConfiguration config) : Repository(config), ITapiDirectoryRepository
{
public async Task<IEnumerable<TapiContact>> SearchAsync(string[] args)
{
await using var con = await OpenAsync("Tapi");
var sql = new StringBuilder("""
SELECT TOP 10
TD_ID,
TD_NAME,
TD_NUMBER,
TD_NUMBER_TAPI,
TD_MEDIUM
FROM dbo.CP_TAPI_DIRECTORY
WHERE
""");
var first = true;
var dp = new DynamicParameters();
for (var i = 1; i <= args.Length; i++)
{
if (!first)
{
sql.AppendLine("AND");
}
first = false;
sql.AppendLine($"(TD_NAME LIKE @s{i} OR");
sql.AppendLine($" TD_NUMBER_TAPI LIKE @s{i})");
dp.Add($"s{i}", $"%{args[i-1]}%");
}
return await con.QueryAsync<TapiContact>(sql.ToString(), dp);
}
public async Task<TapiContact?> SearchByNumberAsync(string number)
{
await using var con = await OpenAsync("Tapi");
var result = await con.QueryFirstOrDefaultAsync<TapiContact>("""
SELECT
TD_ID
,TD_NAME
,TD_NUMBER
,TD_NUMBER_TAPI
,TD_MEDIUM
FROM dbo.CP_TAPI_DIRECTORY
WHERE TD_NUMBER_TAPI LIKE @number
""", new {number});
return result;
}
public async Task<IEnumerable<TapiContact>> GetAllAsync()
{
await using var con = await OpenAsync("Tapi");
return await con.QueryAsync<TapiContact>("""
SELECT
TD_ID
,TD_NAME
,TD_NUMBER
,TD_NUMBER_TAPI
,TD_MEDIUM
FROM dbo.CP_TAPI_DIRECTORY
""");
}
}
@@ -0,0 +1,41 @@
using CPATapi.Server.Interfaces;
using CPATapi.Server.Models;
using Dapper;
namespace CPATapi.Server.Repository;
internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository
{
private const string SelectStampsQuery = """
SELECT
ma.MA_USER_NAME
,bu.LOGGED_IN
,us.US_EXTENSION
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
WHERE bu.BU_MA_NR = ma.MA_NR
AND
BU_BU >= @from AND BU_BU < @to
) bu
WHERE
ma.MA_USER_AKTIV = 1
""";
public async Task<IEnumerable<Availability>> GetUsersAvailabilityAsync(DateTime from, DateTime to)
{
await using var con = await OpenAsync("ZeitConsens");
return await con.QueryAsync<Availability>(SelectStampsQuery, new { from, to });
}
public async Task<Availability?> GetUserAvailabilityAsync(string user, DateTime from, DateTime to)
{
await using var con = await OpenAsync("ZeitConsens");
return await con.QueryFirstOrDefaultAsync<Availability>($"""
{SelectStampsQuery}
AND ma.MA_USER_NAME = @user
""", new { user, from, to });
}
}
@@ -0,0 +1,16 @@
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
},
"ConnectionStrings": {
"Tapi": "",
"ZeitConsens": ""
},
"AllowedHosts": "*"
}
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</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.Client\CPATapi.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
</Project>
@@ -0,0 +1,56 @@
using CPATapi.Client.Models;
using Microsoft.Extensions.DependencyInjection;
namespace CPATapi.Client.Tests;
public class CPATapiClientTests
{
private IServiceScope CreateServices()
{
var services = new ServiceCollection();
services.AddCPATapiClient(((provider, client) =>
{
client.BaseAddress = new Uri("https://3cxtapi.cp-austria.at/");
}));
var sp = services.BuildServiceProvider();
return sp.CreateScope();
}
[Test]
public async Task TestContactsGet()
{
using var scope = CreateServices();
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
var contacts = await client.Contact.GetAsync();
Assert.That(contacts, Is.Not.Null);
Assert.That(contacts, Is.Not.Empty);
}
[Test]
public async Task TestAvailabilityGet()
{
using var scope = CreateServices();
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
var availability = await client.Availability["CPATRD"].GetAsync();
Assert.That(availability, Is.Not.Null);
}
[Test]
public async Task TestAvailabilityGetUnknown()
{
using var scope = CreateServices();
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
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.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"));
}
}