12 Commits

Author SHA1 Message Date
2886ab6522 Serilog 2026-03-19 14:32:20 +01:00
994cc72e1d Bump version to 9.3.0 2026-03-19 14:32:19 +01:00
a02ce50cf3 Change favicon color depending on status 2026-03-19 14:32:19 +01:00
c16c07ea42 New 3cx tapi server url 2026-03-19 14:32:19 +01:00
535cf6ea05 Removed unused imports 2026-03-19 14:32:19 +01:00
766ed86999 Modernize and Dockerize server 2026-03-19 14:32:14 +01:00
76a2bf0e88 Modernized 2026-03-19 13:42:42 +01:00
fe8fcdf45b 3cx Tapi Server 2026-03-19 13:42:36 +01:00
7a99b1ab55 .gitignore aktualisiert 2026-03-18 10:59:12 +01:00
41f6e640e1 Packages aktualisiert 2026-03-18 10:53:51 +01:00
26f1902996 Moved userscript to client 2026-03-18 10:34:20 +01:00
c8b8199e93 No more /git - fixed wrong link 2026-03-18 08:38:08 +01:00
44 changed files with 3468 additions and 2850 deletions

26
.gitignore vendored
View File

@@ -1,4 +1,24 @@
node_modules
.idea .idea
.vscode .vscode/*
dist
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/

File diff suppressed because it is too large Load Diff

View File

@@ -5,17 +5,15 @@ module.exports = {
namespace: 'http://cp-solutions.at', namespace: 'http://cp-solutions.at',
version: pkg.version, version: pkg.version,
author: pkg.author, author: pkg.author,
copyright: 'Copyright 2021 CP Solutions GmbH', copyright: 'Copyright CP Solutions GmbH',
source: pkg.repository.url, source: pkg.repository.url,
downloadURL: 'https://source.cp-austria.at/git/CPATRD/3cx_tapi/raw/branch/master/3CX_TAPI.user.js', downloadURL: `${pkg.repository.url}/raw/branch/master/3CX_TAPI.user.js`,
match: [ match: [
'https://192.168.0.154:5001/*', 'https://192.168.0.154:5001/*',
'https://cpsolution.my3cx.at:5001/*' 'https://cpsolution.my3cx.at:5001/*'
], ],
require: [ require: [
'https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js', 'https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js',
`https://cdn.jsdelivr.net/npm/axios@${pkg.dependencies.axios}/dist/axios.min.js`,
`https://cdn.jsdelivr.net/npm/axios-userscript-adapter@${pkg.dependencies['axios-userscript-adapter']}/dist/axiosGmxhrAdapter.min.js`
], ],
grant: [ grant: [
'GM.xmlHttpRequest', 'GM.xmlHttpRequest',
@@ -24,7 +22,7 @@ module.exports = {
'GM.setValue' 'GM.setValue'
], ],
connect: [ connect: [
'cpatapi.cpsrvweb2016.cp-austria.at' '3cxtapi.cp-austria.at'
], ],
'run-at': 'document-end' 'run-at': 'document-end'
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "3cx-tapi", "name": "3cx-tapi",
"description": "3CX CP Tapi and Projectmanager integration", "description": "3CX CP Tapi and Projectmanager integration",
"version": "9.2.2", "version": "9.3.0",
"author": { "author": {
"name": "Daniel Triendl", "name": "Daniel Triendl",
"email": "d.triendl@cp-solutions.at" "email": "d.triendl@cp-solutions.at"
@@ -20,35 +20,35 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://source.cp-austria.at/git/CPATRD/3cx_tapi.git" "url": "https://source.cp-austria.at/CPATRD/3cx_tapi.git"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"chrono-node": "^2.7.7", "@trim21/gm-fetch": "^0.3.0",
"@trim21/gm-fetch": "^0.1.16" "chrono-node": "^2.9.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.2",
"@types/greasemonkey": "^4.0.7", "@types/greasemonkey": "^4.0.7",
"@babel/core": "^7.25.8", "@typescript-eslint/eslint-plugin": "^8.57.1",
"@babel/preset-env": "^7.25.8", "@typescript-eslint/parser": "^8.57.1",
"@typescript-eslint/eslint-plugin": "^8.8.1", "babel-loader": "^10.1.1",
"@typescript-eslint/parser": "^8.8.1",
"babel-loader": "^9.2.1",
"browserslist": "^4.21.9", "browserslist": "^4.21.9",
"css-loader": "^7.1.2", "css-loader": "^7.1.4",
"eslint": "^9.12.0", "eslint": "^9.39.4",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "^7.1.0S", "eslint-plugin-promise": "^7.2.1",
"less": "4.2.0", "less": "^4.6.4",
"less-loader": "^12.2.0", "less-loader": "^12.3.2",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.4",
"typescript": "^5.6.3", "typescript": "^5.9.3",
"userscript-metadata-webpack-plugin": "^0.4.0", "userscript-metadata-webpack-plugin": "^0.4.2",
"webpack": "^5.95.0", "webpack": "^5.105.4",
"webpack-bundle-analyzer": "^4.10.2", "webpack-bundle-analyzer": "^5.2.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^7.0.2",
"webpack-livereload-plugin": "3.0.2", "webpack-livereload-plugin": "3.0.2",
"webpack-merge": "^6.0.1" "webpack-merge": "^6.0.1"
} }

View File

@@ -43,7 +43,7 @@ npm run build
## distribution ## distribution
``` ```
cp "dist/3CX TAPI.prod.user.js" 3CX_TAPI.user.js cp "dist/3CX TAPI.prod.user.js" ../3CX_TAPI.user.js
``` ```
And commit 3CX_TAPI.user.js And commit 3CX_TAPI.user.js

View File

@@ -2,6 +2,7 @@ import * as chrono from 'chrono-node'
import { TapiContact } from './tapi-contact' import { TapiContact } from './tapi-contact'
import { extractNumber } from './utils' import { extractNumber } from './utils'
import GM_fetch from '@trim21/gm-fetch' import GM_fetch from '@trim21/gm-fetch'
import { Config } from './config'
export class CallHistory { export class CallHistory {
private callerIds: { [number: string]: TapiContact } = {} private callerIds: { [number: string]: TapiContact } = {}
@@ -86,7 +87,7 @@ export class CallHistory {
if (this.callerIds[number] !== undefined) { if (this.callerIds[number] !== undefined) {
this.updateCallHistoryEntry(element, this.callerIds[number]) this.updateCallHistoryEntry(element, this.callerIds[number])
} else { } else {
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/callerid/' + encodeURIComponent(number)) var response = await GM_fetch(Config.tapi_server_url + '/callerid/' + encodeURIComponent(number))
var callerId: TapiContact = { tD_NAME: '' } var callerId: TapiContact = { tD_NAME: '' }
if (response.status === 200) { if (response.status === 200) {
callerId = await response.json() as TapiContact callerId = await response.json() as TapiContact

View File

@@ -1,6 +1,7 @@
import GM_fetch from '@trim21/gm-fetch' import GM_fetch from '@trim21/gm-fetch'
import { TapiContact } from './tapi-contact' import { TapiContact } from './tapi-contact'
import { extractNumber } from './utils' import { extractNumber } from './utils'
import { Config } from './config'
export class CallNotification { export class CallNotification {
public async showCallNotification (element: HTMLElement) { public async showCallNotification (element: HTMLElement) {
@@ -14,7 +15,7 @@ export class CallNotification {
} }
console.log('TAPI searching callerid for', number) console.log('TAPI searching callerid for', number)
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/callerid/' + encodeURIComponent(number)) var response = await GM_fetch(Config.tapi_server_url + '/callerid/' + encodeURIComponent(number))
console.log('TAPI callerid response', response) console.log('TAPI callerid response', response)
var notification = { var notification = {
text: number text: number

5
client/src/config.ts Normal file
View File

@@ -0,0 +1,5 @@
class _Config {
public tapi_server_url: string = 'https://3cxtapi.cp-austria.at'
}
export const Config = new _Config()

View File

@@ -22,3 +22,5 @@ waitForKeyElements('.call-history-list call', (element) => { callHistory.showCal
const status = new Status() const status = new Status()
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false) waitForKeyElements('wc-account-menu', (element) => { status.showStatus(element) }, false)
waitForKeyElements('wc-account-menu i.status-indicator', (element) => { status.watchStatus(element) }, false)

View File

@@ -3,6 +3,7 @@ import { TapiContact } from './tapi-contact'
import { debounce } from './debounce' import { debounce } from './debounce'
import { fireChangeEvents } from './utils' import { fireChangeEvents } from './utils'
import GM_fetch from '@trim21/gm-fetch' import GM_fetch from '@trim21/gm-fetch'
import { Config } from './config'
export class Search { export class Search {
private currentSearchText = '' private currentSearchText = ''
@@ -103,7 +104,7 @@ export class Search {
return return
} }
console.log('Searching TAPI') console.log('Searching TAPI')
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/search?query=' + encodeURIComponent(searchText)) var response = await GM_fetch(Config.tapi_server_url + '/search?query=' + encodeURIComponent(searchText))
console.log('TAPI Search response', response) console.log('TAPI Search response', response)
var contacts = await response.json() as TapiContact[] var contacts = await response.json() as TapiContact[]
console.log('TAPI Contacts', contacts) console.log('TAPI Contacts', contacts)

View File

@@ -1,3 +1,4 @@
import { Config } from './config';
import './status.css'; import './status.css';
import { ZcStatus } from './zc-status'; import { ZcStatus } from './zc-status';
import GM_fetch from "@trim21/gm-fetch"; import GM_fetch from "@trim21/gm-fetch";
@@ -26,7 +27,7 @@ export class Status {
private async checkStatus() { private async checkStatus() {
if (this._enabled) { if (this._enabled) {
try { try {
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/availability/' + encodeURIComponent(this._user)); var response = await GM_fetch(Config.tapi_server_url + '/availability/' + encodeURIComponent(this._user));
if (response.status == 200) { if (response.status == 200) {
var status = await response.json() as ZcStatus; var status = await response.json() as ZcStatus;
if (this._currentStatus !== status.loggedIn) { if (this._currentStatus !== status.loggedIn) {
@@ -156,4 +157,34 @@ export class Status {
this._currentStatus = undefined; 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";
}
} }

30
server/.dockerignore Normal file
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/**

25
server/3cx_Tapi.sln Normal file
View 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

View 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>

View File

@@ -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 });
}
}

View 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();
}
}

View 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());
}
}

View 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));
}
}

View 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"]

View File

@@ -0,0 +1,5 @@
namespace CPATapi.Server.Interfaces;
public interface IRepository
{
}

View File

@@ -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();
}

View File

@@ -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);
}

View 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; }
}

View File

@@ -0,0 +1,7 @@
namespace CPATapi.Server.Models;
public class Stamp
{
public int MA_NR { get; set; }
public DateTime BU_BU { get; set; }
}

View 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; }
}

View 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();

View 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
}
}
}

View 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;
}
}

View File

@@ -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
""");
}
}

View File

@@ -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 });
}
}

View File

@@ -0,0 +1,16 @@
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
},
"ConnectionStrings": {
"Tapi": "",
"ZeitConsens": ""
},
"AllowedHosts": "*"
}

View 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