2 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
15 changed files with 200 additions and 44 deletions
+6
View File
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client", "src\CPATa
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client.Tests", "test\CPATapi.Client.Tests\CPATapi.Client.Tests.csproj", "{17F37791-4F68-46D5-8CF5-5F1736F6776E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Client.Tests", "test\CPATapi.Client.Tests\CPATapi.Client.Tests.csproj", "{17F37791-4F68-46D5-8CF5-5F1736F6776E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CPATapi.Server.Tests", "test\CPATapi.Server.Tests\CPATapi.Server.Tests.csproj", "{72486DC9-2C7D-409B-9E14-6D90F67B92CC}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -30,6 +32,10 @@ Global
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Debug|Any CPU.Build.0 = Debug|Any CPU {17F37791-4F68-46D5-8CF5-5F1736F6776E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.ActiveCfg = Release|Any CPU {17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.Build.0 = Release|Any CPU {17F37791-4F68-46D5-8CF5-5F1736F6776E}.Release|Any CPU.Build.0 = Release|Any CPU
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72486DC9-2C7D-409B-9E14-6D90F67B92CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -1,12 +1,14 @@
// <auto-generated/> // <auto-generated/>
#pragma warning disable CS0618 #pragma warning disable CS0618
using CPATapi.Client.Availability.Item; using CPATapi.Client.Availability.Item;
using CPATapi.Client.Availability.Users; using CPATapi.Client.Models;
using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading;
using System; using System;
namespace CPATapi.Client.Availability namespace CPATapi.Client.Availability
{ {
@@ -16,11 +18,6 @@ namespace CPATapi.Client.Availability
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class AvailabilityRequestBuilder : BaseRequestBuilder public partial class AvailabilityRequestBuilder : BaseRequestBuilder
{ {
/// <summary>The users property</summary>
public global::CPATapi.Client.Availability.Users.UsersRequestBuilder Users
{
get => new global::CPATapi.Client.Availability.Users.UsersRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>Gets an item from the CPATapi.Client.Availability.item collection</summary> /// <summary>Gets an item from the CPATapi.Client.Availability.item collection</summary>
/// <param name="position">Unique identifier of the item</param> /// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/></returns> /// <returns>A <see cref="global::CPATapi.Client.Availability.Item.WithUserItemRequestBuilder"/></returns>
@@ -49,6 +46,55 @@ namespace CPATapi.Client.Availability
public AvailabilityRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability", rawUrl) public AvailabilityRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/Availability", rawUrl)
{ {
} }
/// <returns>A List&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 #pragma warning restore CS0618
@@ -36,6 +36,7 @@ namespace CPATapi.Client.Availability.Item
/// <returns>A <see cref="global::CPATapi.Client.Models.Availability"/></returns> /// <returns>A <see cref="global::CPATapi.Client.Models.Availability"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param> /// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param> /// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::CPATapi.Client.Models.ProblemDetails">When receiving a 404 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable #nullable enable
public async Task<global::CPATapi.Client.Models.Availability?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default) public async Task<global::CPATapi.Client.Models.Availability?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
@@ -46,7 +47,11 @@ namespace CPATapi.Client.Availability.Item
{ {
#endif #endif
var requestInfo = ToGetRequestInformation(requestConfiguration); var requestInfo = ToGetRequestInformation(requestConfiguration);
return await RequestAdapter.SendAsync<global::CPATapi.Client.Models.Availability>(requestInfo, global::CPATapi.Client.Models.Availability.CreateFromDiscriminatorValue, default, cancellationToken).ConfigureAwait(false); var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "404", global::CPATapi.Client.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync<global::CPATapi.Client.Models.Availability>(requestInfo, global::CPATapi.Client.Models.Availability.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
} }
/// <returns>A <see cref="RequestInformation"/></returns> /// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param> /// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
@@ -7,7 +7,7 @@
<PackageId>CPATapi.Client</PackageId> <PackageId>CPATapi.Client</PackageId>
<Authors>Daniel Triendl</Authors> <Authors>Daniel Triendl</Authors>
<Company>CP Solutions GmbH</Company> <Company>CP Solutions GmbH</Company>
<Version>9.5.1</Version> <Version>9.6.0</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup> </PropertyGroup>
@@ -14,6 +14,14 @@ namespace CPATapi.Client.Models
{ {
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary> /// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
public IDictionary<string, object> AdditionalData { get; set; } public IDictionary<string, object> AdditionalData { get; set; }
/// <summary>The extension property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Extension { get; set; }
#nullable restore
#else
public string Extension { get; set; }
#endif
/// <summary>The loggedIn property</summary> /// <summary>The loggedIn property</summary>
public bool? LoggedIn { get; set; } public bool? LoggedIn { get; set; }
/// <summary>The user property</summary> /// <summary>The user property</summary>
@@ -49,6 +57,7 @@ namespace CPATapi.Client.Models
{ {
return new Dictionary<string, Action<IParseNode>> return new Dictionary<string, Action<IParseNode>>
{ {
{ "extension", n => { Extension = n.GetStringValue(); } },
{ "loggedIn", n => { LoggedIn = n.GetBoolValue(); } }, { "loggedIn", n => { LoggedIn = n.GetBoolValue(); } },
{ "user", n => { User = n.GetStringValue(); } }, { "user", n => { User = n.GetStringValue(); } },
}; };
@@ -60,6 +69,7 @@ namespace CPATapi.Client.Models
public virtual void Serialize(ISerializationWriter writer) public virtual void Serialize(ISerializationWriter writer)
{ {
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer)); if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("extension", Extension);
writer.WriteBoolValue("loggedIn", LoggedIn); writer.WriteBoolValue("loggedIn", LoggedIn);
writer.WriteStringValue("user", User); writer.WriteStringValue("user", User);
writer.WriteAdditionalData(AdditionalData); writer.WriteAdditionalData(AdditionalData);
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"descriptionHash": "1B47F98D82C5E24FB3ABEDB3BF90615424A8F19620EA2621D23B7FC69F4F7BF27D512A11098EF4D8940C7D243DEEB59C0CA038264430AD9150B612F5046C8146", "descriptionHash": "3ADB4B190A2637B9EC01981B2508C539F2A582D95310D01FF97D2F2C068B9024CDC66F4D14F486265ED22314E9EEB2EA7CD3BF0F3D1ECC061BA7B9734B520A9D",
"descriptionLocation": "../CPATapi.Server/CPATapi.Server.json", "descriptionLocation": "../CPATapi.Server/CPATapi.Server.json",
"lockFileVersion": "1.0.0", "lockFileVersion": "1.0.0",
"kiotaVersion": "1.30.0", "kiotaVersion": "1.30.0",
@@ -8,7 +8,7 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext> <DockerfileContext>..\..</DockerfileContext>
<OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory> <OpenApiDocumentsDirectory>.</OpenApiDocumentsDirectory>
<Version>9.5.1</Version> <Version>9.6.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -30,4 +30,8 @@
<Folder Include="wwwroot\" /> <Folder Include="wwwroot\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="CPATapi.Server.Tests" />
</ItemGroup>
</Project> </Project>
@@ -11,19 +11,24 @@ namespace CPATapi.Server.Controllers;
public class AvailabilityController(IZeitConsensRepository zeitConsens) : ControllerBase public class AvailabilityController(IZeitConsensRepository zeitConsens) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Route("users")] [Route("")]
[ProducesResponseType<IEnumerable<string>>(StatusCodes.Status200OK)] [ProducesResponseType<IEnumerable<Availability>>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetUsers() public async Task<IActionResult> GetUsers()
{ {
return Ok(await zeitConsens.GetUsersAsync()); return Ok(await zeitConsens.GetUsersAvailabilityAsync(DateTime.Now.Date, DateTime.Now.AddDays(1).Date));
} }
[HttpGet] [HttpGet]
[Route("{user}")] [Route("{user}")]
[ProducesResponseType<Availability>(StatusCodes.Status200OK)] [ProducesResponseType<Availability>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetAvailability(string user) public async Task<IActionResult> GetAvailability(string user)
{ {
var stampCount = (await zeitConsens.GetStampsAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date)).Count(); var availability = await zeitConsens.GetUserAvailabilityAsync(user, DateTime.Now.Date, DateTime.Now.AddDays(1).Date);
return Ok(new Availability { User = user, LoggedIn = stampCount % 2 != 0 }); if (availability == null)
{
return NotFound();
}
return Ok(availability);
} }
} }
@@ -4,6 +4,6 @@ namespace CPATapi.Server.Interfaces;
public interface IZeitConsensRepository : IRepository public interface IZeitConsensRepository : IRepository
{ {
Task<IEnumerable<string>> GetUsersAsync(); Task<IEnumerable<Availability>> GetUsersAvailabilityAsync(DateTime from, DateTime to);
Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to); Task<Availability?> GetUserAvailabilityAsync(string user, DateTime from, DateTime to);
} }
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CPATapi.Server.Models; namespace CPATapi.Server.Models;
@@ -5,7 +6,9 @@ namespace CPATapi.Server.Models;
public class Availability public class Availability
{ {
[JsonPropertyName("user")] [JsonPropertyName("user")]
public string User { get; set; } public required string MA_USER_NAME { get; set; }
[JsonPropertyName("loggedIn")] [JsonPropertyName("loggedIn")]
public bool LoggedIn { get; set; } public bool LOGGED_IN { get; set; }
[JsonPropertyName("extension")]
public string? US_EXTENSION { get; set; }
} }
@@ -1,7 +0,0 @@
namespace CPATapi.Server.Models;
public class Stamp
{
public int MA_NR { get; set; }
public DateTime BU_BU { get; set; }
}
@@ -6,29 +6,36 @@ namespace CPATapi.Server.Repository;
internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository internal class ZeitConsensRepository(IConfiguration config) : Repository(config), IZeitConsensRepository
{ {
public async Task<IEnumerable<string>> GetUsersAsync() private const string SelectStampsQuery = """
SELECT
ma.MA_USER_NAME
,bu.LOGGED_IN
,us.US_EXTENSION
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"); await using var con = await OpenAsync("ZeitConsens");
return await con.QueryAsync<string>(""" return await con.QueryAsync<Availability>(SelectStampsQuery, new { from, to });
SELECT DISTINCT MA_USER_NAME
FROM dbo.MA_DATEN
WHERE MA_USER_NAME IS NOT NULL AND MA_USER_AKTIV = 1
ORDER BY MA_USER_NAME
""");
} }
public async Task<IEnumerable<Stamp>> GetStampsAsync(string user, DateTime from, DateTime to) public async Task<Availability?> GetUserAvailabilityAsync(string user, DateTime from, DateTime to)
{ {
await using var con = await OpenAsync("ZeitConsens"); await using var con = await OpenAsync("ZeitConsens");
return await con.QueryAsync<Stamp>(""" return await con.QueryFirstOrDefaultAsync<Availability>($"""
SELECT {SelectStampsQuery}
MA_NR AND ma.MA_USER_NAME = @user
,BU_BU
FROM dbo.BU
INNER JOIN dbo.MA_DATEN on MA_NR = BU_MA_NR
WHERE
MA_USER_NAME = @user AND
BU_BU >= @from AND BU_BU < @to
""", new { user, from, to }); """, new { user, from, to });
} }
} }
@@ -1,3 +1,4 @@
using CPATapi.Client.Models;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace CPATapi.Client.Tests; namespace CPATapi.Client.Tests;
@@ -39,7 +40,17 @@ public class CPATapiClientTests
{ {
using var scope = CreateServices(); using var scope = CreateServices();
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>(); var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
var availability = await client.Availability["Unknown"].GetAsync(); Assert.ThrowsAsync<ProblemDetails>(async () => await client.Availability["Unknown"].GetAsync());
}
[Test]
public async Task TestAvailabilityGetAll()
{
using var scope = CreateServices();
var client = scope.ServiceProvider.GetRequiredService<CPATapiClient>();
var availability = await client.Availability.GetAsync();
Assert.That(availability, Is.Not.Null); Assert.That(availability, Is.Not.Null);
Assert.That(availability, Is.Not.Empty);
Assert.That(availability, Has.Count.GreaterThan(1));
} }
} }
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>a7b40068-a2f6-4c63-bbd9-0fd346908fb0</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CPATapi.Server\CPATapi.Server.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
</Project>
@@ -0,0 +1,38 @@
using Microsoft.Extensions.Configuration;
using CPATapi.Server.Repository;
namespace CPATapi.Server.Tests;
public class ZeitConsensRepositoryTest
{
[OneTimeSetUp]
public void Setup()
{
// the type specified here is just so the secrets library can
// find the UserSecretId we added in the csproj file
var builder = new ConfigurationBuilder().AddUserSecrets<ZeitConsensRepositoryTest>();
_configuration = builder.Build();
}
private IConfigurationRoot _configuration { get; set; }
[Test]
public async Task TestGetUsersAvailabilityAsync()
{
var zcRepo = new ZeitConsensRepository(_configuration);
var availability= (await zcRepo.GetUsersAvailabilityAsync(DateTime.Now.Date, DateTime.Now.AddDays(1).Date)).ToList();
Assert.That(availability, Is.Not.Empty);
Assert.That(availability, Has.Count.GreaterThan(1));
}
[Test]
public async Task TestGetUserAvailabilityAsync()
{
var zcRepo = new ZeitConsensRepository(_configuration);
var availability = await zcRepo.GetUserAvailabilityAsync("CPATRD", DateTime.Now.Date, DateTime.Now.AddDays(1).Date);
Assert.That(availability, Is.Not.Null);
Assert.That(availability.MA_USER_NAME, Is.EqualTo("CPATRD"));
Assert.That(availability.US_EXTENSION, Is.EqualTo("203"));
}
}