Compare commits
57 Commits
v1.0.0
...
2886ab6522
| Author | SHA1 | Date | |
|---|---|---|---|
| 2886ab6522 | |||
| 994cc72e1d | |||
| a02ce50cf3 | |||
| c16c07ea42 | |||
| 535cf6ea05 | |||
| 766ed86999 | |||
| 76a2bf0e88 | |||
| fe8fcdf45b | |||
| 7a99b1ab55 | |||
| 41f6e640e1 | |||
| 26f1902996 | |||
| c8b8199e93 | |||
| 4a81cbf321 | |||
| b1d846de32 | |||
| 748a8740eb | |||
| 248fbd5f0f | |||
| 20e011bb55 | |||
| bbb4a910a0 | |||
| 1cbde09ac6 | |||
| d107b1a49f | |||
| 505bab6d34 | |||
| e1459856c2 | |||
| 72e7a95904 | |||
| 69e5857963 | |||
| 9a0c476bc5 | |||
| e563279faf | |||
| 856181f530 | |||
| ae0c125a50 | |||
| ba5a5c627b | |||
| 3bf1baeca8 | |||
| 6186b14b16 | |||
| f578bd2fe1 | |||
| 933b445ed6 | |||
| bbe20d6351 | |||
| 9e5d93bad2 | |||
| b83cef625a | |||
| ad5c8ece12 | |||
| 0455cb1926 | |||
| 3e33155276 | |||
| de34a6c66e | |||
| a4a346b48d | |||
| cd303869c8 | |||
| 4283ee6b5c | |||
|
|
fec9885a64 | ||
|
|
29fc426161 | ||
|
|
231d24b26a | ||
|
|
f3693162ab | ||
|
|
c09bdd856b | ||
|
|
ecb9097f5f | ||
|
|
7db79afca2 | ||
|
|
e40a0810ff | ||
|
|
10c1a9185b | ||
|
|
060889df69 | ||
|
|
0ac9f4d4ae | ||
|
|
44628074c0 | ||
|
|
d49b934c9a | ||
|
|
204d5837c5 |
9
.babelrc.js
Normal file
9
.babelrc.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true)
|
||||||
|
|
||||||
|
const presets = ['@babel/preset-env']
|
||||||
|
|
||||||
|
return {
|
||||||
|
presets,
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
not IE 11
|
||||||
|
not dead
|
||||||
15
.editorconfig
Normal file
15
.editorconfig
Normal 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
.eslintrc.yaml
Normal file
28
.eslintrc.yaml
Normal 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"]
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
.idea
|
||||||
|
.vscode/*
|
||||||
|
|
||||||
|
client/node_modules
|
||||||
|
client/dist
|
||||||
|
|
||||||
|
.vs/
|
||||||
|
.user
|
||||||
|
|
||||||
|
# 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/
|
||||||
4934
3CX_TAPI.user.js
4934
3CX_TAPI.user.js
File diff suppressed because it is too large
Load Diff
0
client/config/empty.cjs
Normal file
0
client/config/empty.cjs
Normal file
28
client/config/metadata.cjs
Normal file
28
client/config/metadata.cjs
Normal 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: `${pkg.repository.url}/raw/branch/master/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'
|
||||||
|
}
|
||||||
51
client/config/webpack.config.base.cjs
Normal file
51
client/config/webpack.config.base.cjs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: process.env.npm_config_report ? [new BundleAnalyzerPlugin()] : [],
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = webpackConfig
|
||||||
37
client/config/webpack.config.dev.cjs
Normal file
37
client/config/webpack.config.dev.cjs
Normal 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
|
||||||
19
client/config/webpack.config.production.cjs
Normal file
19
client/config/webpack.config.production.cjs
Normal file
@@ -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
|
||||||
7539
client/package-lock.json
generated
Normal file
7539
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
client/package.json
Normal file
55
client/package.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "3cx-tapi",
|
||||||
|
"description": "3CX CP Tapi and Projectmanager integration",
|
||||||
|
"version": "9.3.0",
|
||||||
|
"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",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
client/readme.md
Normal file
53
client/readme.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# 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/index.prod.user.js` is the final script.
|
||||||
|
|
||||||
|
## distribution
|
||||||
|
|
||||||
|
```
|
||||||
|
cp "dist/3CX TAPI.prod.user.js" ../3CX_TAPI.user.js
|
||||||
|
```
|
||||||
|
|
||||||
|
And commit 3CX_TAPI.user.js
|
||||||
|
|
||||||
|
## see also
|
||||||
|
|
||||||
|
Based on [webpack-userscript-template](https://github.com/Trim21/webpack-userscript-template/)
|
||||||
100
client/src/call-history.ts
Normal file
100
client/src/call-history.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
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 = '<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>'
|
||||||
|
|
||||||
|
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
client/src/call-notification.ts
Normal file
33
client/src/call-notification.ts
Normal 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
client/src/config.ts
Normal file
5
client/src/config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class _Config {
|
||||||
|
public tapi_server_url: string = 'https://3cxtapi.cp-austria.at'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Config = new _Config()
|
||||||
13
client/src/debounce.js
Normal file
13
client/src/debounce.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
client/src/index.js
Normal file
26
client/src/index.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 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 { 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 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-history-list 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)
|
||||||
42
client/src/search.css
Normal file
42
client/src/search.css
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.tapi-search-autocomplete {
|
||||||
|
/*the container must be positioned relative:*/
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.tapi-search-autocomplete input {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
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;
|
||||||
|
}
|
||||||
154
client/src/search.ts
Normal file
154
client/src/search.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
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.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
client/src/status.css
Normal file
19
client/src/status.css
Normal 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;
|
||||||
|
}
|
||||||
190
client/src/status.ts
Normal file
190
client/src/status.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { Config } from './config';
|
||||||
|
import './status.css';
|
||||||
|
import { ZcStatus } from './zc-status';
|
||||||
|
import GM_fetch from "@trim21/gm-fetch";
|
||||||
|
|
||||||
|
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');
|
||||||
|
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.onclick = () => {
|
||||||
|
document.getElementById('zc-modal').classList.toggle('show');
|
||||||
|
}
|
||||||
|
menu.appendChild(link);
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
8
client/src/tapi-contact.ts
Normal file
8
client/src/tapi-contact.ts
Normal 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;
|
||||||
|
}
|
||||||
29
client/src/utils.ts
Normal file
29
client/src/utils.ts
Normal 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
client/src/zc-status.ts
Normal file
4
client/src/zc-status.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export class ZcStatus {
|
||||||
|
public user: string;
|
||||||
|
public loggedIn: boolean;
|
||||||
|
}
|
||||||
10
client/tsconfig.json
Normal file
10
client/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2022",
|
||||||
|
"allowJs": true,
|
||||||
|
"moduleResolution": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
server/.dockerignore
Normal file
30
server/.dockerignore
Normal 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/**
|
||||||
25
server/3cx_Tapi.sln
Normal file
25
server/3cx_Tapi.sln
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 18
|
||||||
|
VisualStudioVersion = 18.5.11612.153 insiders
|
||||||
|
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
|
||||||
|
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
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {E2CF5765-9038-4ED6-AC5C-1A3E569ABC6C}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
22
server/src/CPATapi.Server/CPATapi.Server.csproj
Normal file
22
server/src/CPATapi.Server/CPATapi.Server.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<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>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="2.1.72" />
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||||
|
<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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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("users")]
|
||||||
|
[ProducesResponseType<IEnumerable<string>>(StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetUsers()
|
||||||
|
{
|
||||||
|
return Ok(await zeitConsens.GetUsersAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{user}")]
|
||||||
|
[ProducesResponseType<Availability>(StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetAvailability(string user)
|
||||||
|
{
|
||||||
|
var stampCount = (await zeitConsens.GetStampsAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date)).Count();
|
||||||
|
return Ok(new Availability { User = user, LoggedIn = stampCount % 2 != 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
30
server/src/CPATapi.Server/Controllers/CallerIdController.cs
Normal file
30
server/src/CPATapi.Server/Controllers/CallerIdController.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
server/src/CPATapi.Server/Controllers/ContactController.cs
Normal file
21
server/src/CPATapi.Server/Controllers/ContactController.cs
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
25
server/src/CPATapi.Server/Controllers/SearchController.cs
Normal file
25
server/src/CPATapi.Server/Controllers/SearchController.cs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
server/src/CPATapi.Server/Dockerfile
Normal file
28
server/src/CPATapi.Server/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 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 service project
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["CPATapi.Server/CPATapi.Server.csproj", "CPATapi.Server/"]
|
||||||
|
RUN dotnet restore "CPATapi.Server/CPATapi.Server.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/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 .
|
||||||
|
ENTRYPOINT ["dotnet", "CPATapi.Server.dll"]
|
||||||
5
server/src/CPATapi.Server/Interfaces/IRepository.cs
Normal file
5
server/src/CPATapi.Server/Interfaces/IRepository.cs
Normal file
@@ -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<string>> GetUsersAsync();
|
||||||
|
Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to);
|
||||||
|
}
|
||||||
11
server/src/CPATapi.Server/Models/Availability.cs
Normal file
11
server/src/CPATapi.Server/Models/Availability.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CPATapi.Server.Models;
|
||||||
|
|
||||||
|
public class Availability
|
||||||
|
{
|
||||||
|
[JsonPropertyName("user")]
|
||||||
|
public string User { get; set; }
|
||||||
|
[JsonPropertyName("loggedIn")]
|
||||||
|
public bool LoggedIn { get; set; }
|
||||||
|
}
|
||||||
7
server/src/CPATapi.Server/Models/Stamp.cs
Normal file
7
server/src/CPATapi.Server/Models/Stamp.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CPATapi.Server.Models;
|
||||||
|
|
||||||
|
public class Stamp
|
||||||
|
{
|
||||||
|
public int MA_NR { get; set; }
|
||||||
|
public DateTime BU_BU { get; set; }
|
||||||
|
}
|
||||||
10
server/src/CPATapi.Server/Models/TapiContact.cs
Normal file
10
server/src/CPATapi.Server/Models/TapiContact.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
35
server/src/CPATapi.Server/Program.cs
Normal file
35
server/src/CPATapi.Server/Program.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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.AddTransient<ITapiDirectoryRepository, TapiDirectoryRepository>();
|
||||||
|
builder.Services.AddTransient<IZeitConsensRepository, ZeitConsensRepository>();
|
||||||
|
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseSerilogRequestLogging();
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
40
server/src/CPATapi.Server/Properties/launchSettings.json
Normal file
40
server/src/CPATapi.Server/Properties/launchSettings.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "contact",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
server/src/CPATapi.Server/Repository/Repository.cs
Normal file
21
server/src/CPATapi.Server/Repository/Repository.cs
Normal file
@@ -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,34 @@
|
|||||||
|
using CPATapi.Server.Interfaces;
|
||||||
|
using CPATapi.Server.Models;
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace CPATapi.Server.Repository;
|
||||||
|
|
||||||
|
internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<string>> GetUsersAsync()
|
||||||
|
{
|
||||||
|
await using var con = await OpenAsync("ZeitConsens");
|
||||||
|
return await con.QueryAsync<string>("""
|
||||||
|
SELECT DISTINCT MA_USER_NAME
|
||||||
|
FROM dbo.MA_DATEN
|
||||||
|
WHERE MA_USER_NAME IS NOT NULL AND MA_USER_AKTIV = 1
|
||||||
|
ORDER BY MA_USER_NAME
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to)
|
||||||
|
{
|
||||||
|
await using var con = await OpenAsync("ZeitConsens");
|
||||||
|
return await con.QueryAsync<Stamp>("""
|
||||||
|
SELECT
|
||||||
|
MA_NR
|
||||||
|
,BU_BU
|
||||||
|
FROM dbo.BU
|
||||||
|
INNER JOIN dbo.MA_DATEN on MA_NR = BU_MA_NR
|
||||||
|
WHERE
|
||||||
|
MA_USER_NAME = @user AND
|
||||||
|
BU_BU >= @from AND BU_BU < @to
|
||||||
|
""", new { user, from, to });
|
||||||
|
}
|
||||||
|
}
|
||||||
16
server/src/CPATapi.Server/appsettings.json
Normal file
16
server/src/CPATapi.Server/appsettings.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"Serilog": {
|
||||||
|
"MinimumLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Override": {
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Tapi": "",
|
||||||
|
"ZeitConsens": ""
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
19
server/src/CPATapi.Server/publish.bat
Normal file
19
server/src/CPATapi.Server/publish.bat
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
cd /d %~dp0
|
||||||
|
|
||||||
|
docker build -t source.cp-austria.at/cpatrd/3cx_tapi:latest -f 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
|
||||||
Reference in New Issue
Block a user