* Maintain FaKrosno api to handle Hangfire

* Added checking of missing EdiCo based on EdiCoTranslate and send email every 30 minutes
* Added Admin Scheduler view
This commit is contained in:
2025-02-14 08:46:56 +01:00
parent e7342abadd
commit c0fed5b3ec
38 changed files with 446 additions and 221 deletions

View File

@@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using TaskScheduler = AzureDataModel.Entities.TaskScheduler;
namespace AzureDataModel;
public class AzureDbContext : DbContext
{
public AzureDbContext(DbContextOptions<AzureDbContext> options)
: base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var connectionString = configuration.GetConnectionString("AzureConnection");
optionsBuilder.UseAzureSql(connectionString, options => options.CommandTimeout(300));
}
public DbSet<TaskScheduler> TaskSchedulers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TaskScheduler>()
.HasKey(e => e.Id);
modelBuilder.Entity<TaskScheduler>()
.Property(e => e.RowPointer)
.HasDefaultValueSql("newid()");
modelBuilder.Entity<TaskScheduler>()
.Property(e => e.CreateDate)
.HasDefaultValueSql("getdate()");
}
}

View File

@@ -1,8 +0,0 @@
using AzureDataModel.Dtos;
namespace AzureDataModel.Services;
public interface ITaskSchedulerService
{
Task<IEnumerable<TaskSchedulerDto>> GetTaskSchedulers();
}

View File

@@ -1,5 +0,0 @@
{
"ConnectionStrings": {
"AzureConnection": "Server=tcp:fakrosno-hangfire.database.windows.net,1433;Initial Catalog=FaKrosnoHangfire;Persist Security Info=False;User ID=fakrosno_hangfire@trentblaugranagmail.onmicrosoft.com;Password=juhrot-zigQuz-6jydpu;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Authentication=\"Active Directory Password\";"
}
}

View File

@@ -13,9 +13,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FaKrosnoEfDataModel", "FaKr
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SytelineSaAppEfDataModel", "SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj", "{02B7F642-62C1-4BA2-87B0-0B9214319095}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SytelineSaAppEfDataModel", "SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj", "{02B7F642-62C1-4BA2-87B0-0B9214319095}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HangfireApi", "HangfireApi\HangfireApi.csproj", "{52106FF2-E714-4FFB-B383-38B1E782B352}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrdersManagementDataModel", "OrdersManagementDataModel\OrdersManagementDataModel.csproj", "{5971D564-5108-4777-8273-DDE90BF39879}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDataModel", "AzureDataModel\AzureDataModel.csproj", "{5971D564-5108-4777-8273-DDE90BF39879}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,10 +37,6 @@ Global
{02B7F642-62C1-4BA2-87B0-0B9214319095}.Debug|Any CPU.Build.0 = Debug|Any CPU {02B7F642-62C1-4BA2-87B0-0B9214319095}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02B7F642-62C1-4BA2-87B0-0B9214319095}.Release|Any CPU.ActiveCfg = Release|Any CPU {02B7F642-62C1-4BA2-87B0-0B9214319095}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02B7F642-62C1-4BA2-87B0-0B9214319095}.Release|Any CPU.Build.0 = Release|Any CPU {02B7F642-62C1-4BA2-87B0-0B9214319095}.Release|Any CPU.Build.0 = Release|Any CPU
{52106FF2-E714-4FFB-B383-38B1E782B352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{52106FF2-E714-4FFB-B383-38B1E782B352}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52106FF2-E714-4FFB-B383-38B1E782B352}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52106FF2-E714-4FFB-B383-38B1E782B352}.Release|Any CPU.Build.0 = Release|Any CPU
{5971D564-5108-4777-8273-DDE90BF39879}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5971D564-5108-4777-8273-DDE90BF39879}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5971D564-5108-4777-8273-DDE90BF39879}.Debug|Any CPU.Build.0 = Debug|Any CPU {5971D564-5108-4777-8273-DDE90BF39879}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5971D564-5108-4777-8273-DDE90BF39879}.Release|Any CPU.ActiveCfg = Release|Any CPU {5971D564-5108-4777-8273-DDE90BF39879}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -1,21 +1,21 @@
using System.Diagnostics; using System.Diagnostics;
using AzureDataModel.Dtos; using FaKrosnoApi.Models;
using AzureDataModel.Services;
using Hangfire; using Hangfire;
using Hangfire.Storage; using Hangfire.Storage;
using HangfireApi.Dtos;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using OrdersManagementDataModel.Dtos;
using OrdersManagementDataModel.Services;
namespace HangfireApi.Controllers; namespace FaKrosnoApi.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class HangfireJobsController(JobStorage jobStorage, IRecurringJobManager recurringJobManager, ITaskSchedulerService service) : ControllerBase public class HangfireJobsController(JobStorage jobStorage, IRecurringJobManager recurringJobManager, ITaskSchedulerService service) : Controller
{ {
[HttpGet("GetJobsToRun")] [HttpGet("GetJobsToRun")]
public async Task<ActionResult<IEnumerable<JobDto>>> GetJobsToRun() public async Task<ActionResult<IEnumerable<JobModel>>> GetJobsToRun()
{ {
IList<JobDto> jobsToRun = new List<JobDto>(); IList<JobModel> jobsToRun = new List<JobModel>();
using (IStorageConnection? connection = jobStorage.GetConnection()) using (IStorageConnection? connection = jobStorage.GetConnection())
{ {
@@ -28,7 +28,7 @@ public class HangfireJobsController(JobStorage jobStorage, IRecurringJobManager
if (taskScheduler != null) if (taskScheduler != null)
{ {
jobsToRun.Add(new JobDto(recurringJob.Id, recurringJob.Cron, taskScheduler.Path, jobsToRun.Add(new JobModel(recurringJob.Id, recurringJob.Cron, taskScheduler.Path,
recurringJob.LastExecution, recurringJob.NextExecution, recurringJob.Job)); recurringJob.LastExecution, recurringJob.NextExecution, recurringJob.Job));
} }
} }
@@ -59,6 +59,49 @@ public class HangfireJobsController(JobStorage jobStorage, IRecurringJobManager
return Ok("Zadania zostały zaplanowane do uruchamiania zgodnie z ich CRON."); return Ok("Zadania zostały zaplanowane do uruchamiania zgodnie z ich CRON.");
} }
[HttpPost("AddTask")]
public async Task<IActionResult> AddTask([FromBody] TaskSchedulerDto taskSchedulerDto)
{
var taskScheduler = new OrdersManagementDataModel.Entities.TaskScheduler
{
Name = taskSchedulerDto.Name,
Path = taskSchedulerDto.Path,
CronOptions = taskSchedulerDto.CronOptions,
CreateDate = DateTime.UtcNow
};
int result = await service.AddTaskScheduler(taskSchedulerDto);
if (result == 0)
{
return BadRequest("Nie udało się dodać zadania.");
}
recurringJobManager.AddOrUpdate(taskScheduler.Name, () => RunConsoleApplication(taskScheduler.Path),
taskScheduler.CronOptions, new RecurringJobOptions { TimeZone = TimeZoneInfo.Local });
return Ok("Zadanie zostało dodane.");
}
[HttpGet("GetTasks")]
public async Task<ActionResult<IEnumerable<TaskSchedulerDto>>> GetTasks()
{
var tasks = await service.GetTaskSchedulers();
// foreach (TaskSchedulerDto taskSchedulerDto in tasks)
// {
// taskSchedulerDto.JobDetails = GetJob(taskSchedulerDto.Name);
// }
return Ok(tasks);
}
private JobData? GetJob(string jobId)
{
using IStorageConnection? connection = jobStorage.GetConnection();
return connection.GetJobData(jobId);
}
private void RunConsoleApplication(string pathToApp) private void RunConsoleApplication(string pathToApp)
{ {
try try

View File

@@ -0,0 +1,17 @@
using FaKrosnoApi.Services;
using Microsoft.AspNetCore.Mvc;
namespace FaKrosnoApi.Controllers;
[ApiController]
[Route("[controller]")]
public class ScheduleJobController(IScheduleJobService scheduledJob) : ControllerBase
{
[HttpPost("start")]
public IActionResult StartJob()
{
scheduledJob.ExecuteAsync();
return Ok("Job started");
}
}

View File

@@ -7,8 +7,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Hangfire" Version="1.8.17" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -20,6 +22,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FaKrosnoEfDataModel\FaKrosnoEfDataModel.csproj" /> <ProjectReference Include="..\FaKrosnoEfDataModel\FaKrosnoEfDataModel.csproj" />
<ProjectReference Include="..\OrdersManagementDataModel\OrdersManagementDataModel.csproj" />
<ProjectReference Include="..\SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj" /> <ProjectReference Include="..\SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,11 @@
namespace FaKrosnoApi.Models;
public class EmailSettingsModel
{
public string SmtpServer { get; set; }
public int Port { get; set; }
public string SenderEmail { get; set; }
public string SenderPassword { get; set; }
public string RecipientEmail { get; set; }
public bool UseSsl { get; set; }
}

View File

@@ -1,8 +1,8 @@
using Hangfire.Common; using Hangfire.Common;
namespace HangfireApi.Dtos; namespace FaKrosnoApi.Models;
public class JobDto( public class JobModel(
string jobId, string jobId,
string cron, string cron,
string path, string path,

View File

@@ -0,0 +1,6 @@
namespace FaKrosnoApi.Models;
public class JobSettingsModel
{
public int QueryIntervalMinutes { get; set; }
}

View File

@@ -1,35 +1,60 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using FaKrosnoApi.Models;
using FaKrosnoApi.Services;
using FaKrosnoEfDataModel; using FaKrosnoEfDataModel;
using FaKrosnoEfDataModel.Services; using FaKrosnoEfDataModel.Services;
using Hangfire;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using OrdersManagementDataModel;
using OrdersManagementDataModel.Services;
using SytelineSaAppEfDataModel; using SytelineSaAppEfDataModel;
using SytelineSaAppEfDataModel.Services; using SytelineSaAppEfDataModel.Services;
using FaKrosnoMappingProfile = FaKrosnoEfDataModel.MappingProfile; using FaKrosnoMappingProfile = FaKrosnoEfDataModel.MappingProfile;
using SytelineSaAppMappingProfile = SytelineSaAppEfDataModel.MappingProfile; using SytelineSaAppMappingProfile = SytelineSaAppEfDataModel.MappingProfile;
using OrdersManagementMappingProfile = OrdersManagementDataModel.MappingProfile;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
// Add services to the container.
builder.Services.AddDbContext<FaKrosnoDbContext>(options => builder.Services.AddDbContext<FaKrosnoDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("FaKrosnoConnection"))); options.UseSqlServer(builder.Configuration.GetConnectionString("FaKrosnoConnection")));
builder.Services.AddDbContext<SytelineSaAppDbContext>(options => builder.Services.AddDbContext<SytelineSaAppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("SytelineSaAppConnection"))); options.UseSqlServer(builder.Configuration.GetConnectionString("SytelineSaAppConnection")));
builder.Services.AddDbContext<OrdersManagementDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("OrdersManagementConnection")));
builder.Services.Configure<EmailSettingsModel>(builder.Configuration.GetSection("EmailSettings"));
builder.Services.Configure<JobSettingsModel>(builder.Configuration.GetSection("JobSettings"));
builder.WebHost.UseUrls("http://*:5555"); builder.WebHost.UseUrls("http://*:5555");
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config => builder.Services.AddOpenApiDocument(config =>
{ {
config.Title = "FaKrosnoApi"; config.Title = "FaKrosnoApi";
config.Version = "v1"; config.Version = "v1";
}); });
builder.Services.AddHangfire(config => config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("OrdersManagementConnection"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));
builder.Services.AddHangfireServer();
// Configure AutoMapper // Configure AutoMapper
builder.Services.AddAutoMapper(typeof(FaKrosnoMappingProfile), typeof(SytelineSaAppMappingProfile)); builder.Services.AddAutoMapper(typeof(FaKrosnoMappingProfile), typeof(SytelineSaAppMappingProfile),
typeof(OrdersManagementMappingProfile));
// Configure JWT Authentication // Configure JWT Authentication
builder.Services.AddAuthentication(options => builder.Services.AddAuthentication(options =>
@@ -55,15 +80,15 @@ builder.Services.AddScoped<IScheduleOrderDetailsService, ScheduleOrderDetailsSer
builder.Services.AddScoped<IEdiCustomerOrderService, EdiCustomerOrderService>(); builder.Services.AddScoped<IEdiCustomerOrderService, EdiCustomerOrderService>();
builder.Services.AddScoped<IErrorLogService, ErrorLogService>(); builder.Services.AddScoped<IErrorLogService, ErrorLogService>();
builder.Services.AddScoped<ICustomerOrderService, CustomerOrderService>(); builder.Services.AddScoped<ICustomerOrderService, CustomerOrderService>();
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IScheduleJobService, ScheduleJobService>();
builder.Services.AddScoped<ITaskSchedulerService, TaskSchedulerService>();
builder.Services.AddHostedService<TimedHostedService>();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. app.UseOpenApi();
//if (app.Environment.IsDevelopment()) app.UseSwaggerUi();
//{
app.UseOpenApi(); // Serwuje dokument OpenAPI
app.UseSwaggerUi(); // Dodaje interfejs u<>ytkownika Swagger
//}
app.UseHttpsRedirection(); app.UseHttpsRedirection();
@@ -72,4 +97,13 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.UseHangfireDashboard();
// var scopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
// using (var scope = scopeFactory.CreateScope())
// {
// var scheduledJob = scope.ServiceProvider.GetRequiredService<IScheduleJobService>();
// scheduledJob.Start();
// }
app.Run(); app.Run();

View File

@@ -0,0 +1,30 @@
using System.Net;
using System.Net.Mail;
using FaKrosnoApi.Models;
using Microsoft.Extensions.Options;
namespace FaKrosnoApi.Services;
public class EmailService(IOptions<EmailSettingsModel> emailSettings) : IEmailService
{
private readonly EmailSettingsModel _emailSettings = emailSettings.Value;
public void SendEmail(string subject, string body)
{
using var smtpClient = new SmtpClient(_emailSettings.SmtpServer, _emailSettings.Port);
smtpClient.EnableSsl = true;
smtpClient.UseDefaultCredentials = false;
smtpClient.Credentials = new NetworkCredential(_emailSettings.SenderEmail, _emailSettings.SenderPassword);
var mailMessage = new MailMessage
{
From = new MailAddress(_emailSettings.SenderEmail),
Subject = subject,
Body = body
};
mailMessage.To.Add(_emailSettings.RecipientEmail);
smtpClient.Send(mailMessage);
}
}

View File

@@ -0,0 +1,6 @@
namespace FaKrosnoApi.Services;
public interface IEmailService
{
void SendEmail(string subject, string body);
}

View File

@@ -0,0 +1,6 @@
namespace FaKrosnoApi.Services;
public interface IScheduleJobService
{
Task ExecuteAsync();
}

View File

@@ -0,0 +1,29 @@
using System.Text;
using SytelineSaAppEfDataModel.Dtos;
using SytelineSaAppEfDataModel.Services;
namespace FaKrosnoApi.Services;
public class ScheduleJobService(IEmailService emailService, IServiceScopeFactory scopeFactory) : IScheduleJobService
{
public async Task ExecuteAsync()
{
using var scope = scopeFactory.CreateScope();
IEdiCustomerOrderService ediCustomerOrderService = scope.ServiceProvider.GetRequiredService<IEdiCustomerOrderService>();
IEnumerable<EdiCustomerOrderTranslateDto> missingOrders =
(await ediCustomerOrderService.FindMissingOrders(new DateTime(2025, 2, 5))).ToList();
if (missingOrders.Any())
{
StringBuilder result = new StringBuilder();
result.AppendLine("Znaleziono brakujące zamówienia w bazie 'edi_co':");
foreach (EdiCustomerOrderTranslateDto missingOrder in missingOrders)
{
result.AppendLine($"- {missingOrder.EdiCoCoNum}");
}
emailService.SendEmail("Znaleziono brakujące zamówienia!", result.ToString());
}
}
}

View File

@@ -0,0 +1,32 @@
using SytelineSaAppEfDataModel.Services;
namespace FaKrosnoApi.Services;
public class TimedHostedService(IServiceScopeFactory scopeFactory) : IHostedService, IDisposable
{
private Timer? _timer;
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(30));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
using var scope = scopeFactory.CreateScope();
var scheduledJob = scope.ServiceProvider.GetRequiredService<IScheduleJobService>();
scheduledJob.ExecuteAsync();
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}

View File

@@ -1,7 +1,8 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"FaKrosnoConnection": "Server=192.168.0.7;Database=fakrosnotest;User Id=sa;Password=Tetum#2021!;TrustServerCertificate=true", "FaKrosnoConnection": "Server=192.168.0.7;Database=fakrosnotest;User Id=sa;Password=Tetum#2021!;TrustServerCertificate=true",
"SytelineSaAppConnection": "Server=192.168.0.7;Database=SL_PRODTEST_SA_APP;User Id=sa;Password=Tetum#2021!;TrustServerCertificate=true" "SytelineSaAppConnection": "Server=192.168.0.7;Database=SL_PROD_SA_APP;User Id=sa;Password=Tetum#2021!;TrustServerCertificate=true",
"OrdersManagementConnection": "Server=192.168.0.7;Database=OrdersManagement;User Id=sa;Password=Tetum#2021!;TrustServerCertificate=true"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
@@ -16,5 +17,16 @@
}, },
"Host": { "Host": {
"Urls": "http://0.0.0.0:5555" "Urls": "http://0.0.0.0:5555"
},
"EmailSettings": {
"SmtpServer": "poczta.fakrosno.pl",
"Port": 587,
"SenderEmail": "edi@fakrosno.pl",
"SenderPassword": "F@Krosno2014",
"RecipientEmail": "piotr.kus@fakrosno.pl",
"UseSsl": false
},
"JobSettings": {
"QueryIntervalMinutes": 30
} }
} }

View File

@@ -1,9 +1,4 @@
using System; using FaKrosnoEfDataModel.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FaKrosnoEfDataModel.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace FaKrosnoEfDataModel.Queries namespace FaKrosnoEfDataModel.Queries

View File

@@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire" Version="1.8.17" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.12"/>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AzureDataModel\AzureDataModel.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +0,0 @@
@HangfireApi_HostAddress = http://localhost:5237
GET {{HangfireApi_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -1,43 +0,0 @@
using Hangfire;
using Hangfire.SqlServer;
using HangfireApi.Controllers;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));
builder.Services.AddControllers();
builder.Services.AddHangfireServer();
builder.Services.AddAuthorization();
builder.Services.AddSingleton<JobStorage>(provider =>
provider.GetRequiredService<IServiceProvider>().GetService(typeof(JobStorage)) as JobStorage ??
throw new InvalidOperationException());
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.MapHangfireDashboard();
await ((IApplicationBuilder)app).ApplicationServices.GetRequiredService<HangfireJobsController>().RunJobs();
await app.RunAsync();

View File

@@ -1,41 +0,0 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:24492",
"sslPort": 44367
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5237",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7273;http://localhost:5237",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -1,12 +0,0 @@
{
"ConnectionStrings": {
"HangfireConnection": "Server=tcp:fakrosno-hangfire.database.windows.net,1433;Initial Catalog=FaKrosnoHangfire;Persist Security Info=False;User ID=fakrosno_hangfire@trentblaugranagmail.onmicrosoft.com;Password=juhrot-zigQuz-6jydpu;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Authentication=\"Active Directory Password\";"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,51 @@
@page "/Admin/Scheduler"
@using OrdersManagementDataModel.Dtos
@using Syncfusion.Blazor.Grids
@using Syncfusion.Blazor.Inputs
@using Syncfusion.Blazor.Buttons
@inject HangfireService HangfireService
<h3>Zarządzanie Zadaniami</h3>
<div>
<SfTextBox @bind-Value="NewTask.Name" Placeholder="Nazwa zadania"></SfTextBox>
<SfTextBox @bind-Value="NewTask.Path" Placeholder="Ścieżka do aplikacji"></SfTextBox>
<SfTextBox @bind-Value="NewTask.CronOptions" Placeholder="CRON"></SfTextBox>
<SfButton Content="Dodaj Zadanie" @onclick="AddTask"></SfButton>
</div>
<SfGrid DataSource="@Tasks" AllowPaging="true">
<GridColumns>
<GridColumn Field=@nameof(TaskSchedulerDto.Name) HeaderText="Nazwa"></GridColumn>
<GridColumn Field=@nameof(TaskSchedulerDto.Path) HeaderText="Ścieżka"></GridColumn>
<GridColumn Field=@nameof(TaskSchedulerDto.CronOptions) HeaderText="CRON"></GridColumn>
@* <GridColumn Field=@nameof(TaskSchedulerDto.LastExecution) HeaderText="Ostatnie Uruchomienie"></GridColumn> *@
@* <GridColumn Field=@nameof(TaskSchedulerDto.NextExecution) HeaderText="Następne Uruchomienie"></GridColumn> *@
</GridColumns>
</SfGrid>
@code {
private List<TaskSchedulerDto> Tasks { get; set; } = new();
private TaskSchedulerDto NewTask { get; set; } = new();
protected override async Task OnInitializedAsync()
{
await LoadTasks();
}
private async Task AddTask()
{
var response = await HangfireService.AddTaskSchedulerAsync(NewTask);
if (response == 1)
{
NewTask = new TaskSchedulerDto(); // Reset form
await LoadTasks(); // Refresh list
}
}
private async Task LoadTasks()
{
Tasks = (await HangfireService.GetTaskSchedulersAsync() ?? Array.Empty<TaskSchedulerDto>()).ToList();
}
}

View File

@@ -19,6 +19,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\FaKrosnoEfDataModel\FaKrosnoEfDataModel.csproj" /> <ProjectReference Include="..\FaKrosnoEfDataModel\FaKrosnoEfDataModel.csproj" />
<ProjectReference Include="..\OrdersManagementDataModel\OrdersManagementDataModel.csproj" />
<ProjectReference Include="..\SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj" /> <ProjectReference Include="..\SytelineSaAppEfDataModel\SytelineSaAppEfDataModel.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -8,8 +8,8 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSyncfusionBlazor(); builder.Services.AddSyncfusionBlazor();
builder.Services.AddBlazorBootstrap(); builder.Services.AddBlazorBootstrap();
string apiUrl = builder.Configuration["ApiUrl"] ?? "http://localhost:5555"; // Ustawienie na adres twojego API string faKrosnoApiUrl = builder.Configuration["FaKrosnoApiUrl"] ?? "http://localhost:5555"; // Ustawienie na adres twojego API
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiUrl) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(faKrosnoApiUrl) });
// Add services to the container. // Add services to the container.
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
@@ -19,6 +19,7 @@ builder.Services.AddScoped<ScheduleOrderService>();
builder.Services.AddScoped<ScheduleOrderDetailsService>(); builder.Services.AddScoped<ScheduleOrderDetailsService>();
builder.Services.AddScoped<EdiCustomerOrderService>(); builder.Services.AddScoped<EdiCustomerOrderService>();
builder.Services.AddScoped<CustomerOrderService>(); builder.Services.AddScoped<CustomerOrderService>();
builder.Services.AddScoped<HangfireService>();
var app = builder.Build(); var app = builder.Build();

View File

@@ -0,0 +1,22 @@
using OrdersManagementDataModel.Dtos;
namespace OrdersManagement.Services;
public class HangfireService(HttpClient httpClient)
{
public async Task<IEnumerable<TaskSchedulerDto>?> GetTaskSchedulersAsync()
{
return await httpClient.GetFromJsonAsync<IEnumerable<TaskSchedulerDto>>("api/HangfireJobs/GetTasks");
}
public async Task<TaskSchedulerDto?> GetTaskSchedulerAsync(Guid id)
{
return await httpClient.GetFromJsonAsync<TaskSchedulerDto>($"api/HangfireJobs/{id}");
}
public async Task<int> AddTaskSchedulerAsync(TaskSchedulerDto taskSchedulerDto)
{
HttpResponseMessage responseMessage = await httpClient.PostAsJsonAsync("api/HangfireJobs/AddTask", taskSchedulerDto);
return responseMessage.IsSuccessStatusCode ? 1 : 0;
}
}

View File

@@ -1,4 +1,6 @@
namespace AzureDataModel.Dtos; using Hangfire.Storage;
namespace OrdersManagementDataModel.Dtos;
public class TaskSchedulerDto public class TaskSchedulerDto
{ {
@@ -7,4 +9,8 @@ public class TaskSchedulerDto
public string Path { get; set; } public string Path { get; set; }
public string CronOptions { get; set; } public string CronOptions { get; set; }
public DateTime CreateDate { get; set; } public DateTime CreateDate { get; set; }
public DateTime ActiveFrom { get; set; }
public DateTime? ActiveUntil { get; set; }
public JobData? JobDetails { get; set; }
} }

View File

@@ -1,4 +1,4 @@
namespace AzureDataModel.Entities; namespace OrdersManagementDataModel.Entities;
public class TaskScheduler public class TaskScheduler
{ {
@@ -8,4 +8,6 @@ public class TaskScheduler
public string Path { get; set; } public string Path { get; set; }
public string CronOptions { get; set; } public string CronOptions { get; set; }
public DateTime CreateDate { get; set; } public DateTime CreateDate { get; set; }
public DateTime ActiveFrom { get; set; }
public DateTime? ActiveUntil { get; set; }
} }

View File

@@ -1,7 +1,7 @@
using AutoMapper; using AutoMapper;
using AzureDataModel.Dtos; using OrdersManagementDataModel.Dtos;
namespace AzureDataModel; namespace OrdersManagementDataModel;
public class MappingProfile : Profile public class MappingProfile : Profile
{ {

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Hangfire.Core" Version="1.8.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -23,7 +24,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="appsettings_Azure.json"> <None Update="appsettings_OrdersManagement.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using TaskScheduler = OrdersManagementDataModel.Entities.TaskScheduler;
namespace OrdersManagementDataModel;
public class OrdersManagementDbContext : DbContext
{
public OrdersManagementDbContext(DbContextOptions<OrdersManagementDbContext> options)
: base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var connectionString = configuration.GetConnectionString("OrdersManagementConnection");
optionsBuilder.UseSqlServer(connectionString, options => options.CommandTimeout(300));
}
public DbSet<TaskScheduler> TaskSchedulers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TaskScheduler>(entity =>
{
entity.HasKey(e => e.Id);
entity.ToTable("TaskScheduler");
entity.Property(e => e.RowPointer).HasDefaultValueSql("newid()");
entity.Property(e => e.CreateDate).HasDefaultValueSql("getdate()");
entity.Property(e => e.ActiveFrom).HasDefaultValueSql("getdate()");
entity.Property(e => e.ActiveUntil).IsRequired(false); });
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using OrdersManagementDataModel.Dtos;
namespace OrdersManagementDataModel.Queries;
public static class OrdersManagementQueries
{
public static readonly Func<OrdersManagementDbContext, IEnumerable<TaskSchedulerDto>> GetSchedulers =
EF.CompileQuery((OrdersManagementDbContext context) => context.TaskSchedulers.Select(x => new TaskSchedulerDto
{
Id = x.Id,
Name = x.Name,
Path = x.Path,
CronOptions = x.CronOptions,
CreateDate = x.CreateDate,
ActiveFrom = x.ActiveFrom,
ActiveUntil = x.ActiveUntil
}));
}

View File

@@ -0,0 +1,12 @@
using OrdersManagementDataModel.Dtos;
namespace OrdersManagementDataModel.Services;
public interface ITaskSchedulerService
{
Task<IEnumerable<TaskSchedulerDto>> GetTaskSchedulers();
Task<TaskSchedulerDto?> GetTaskSchedulerByTaskName(string taskName);
Task<int> AddTaskScheduler(TaskSchedulerDto taskSchedulerDto);
Task<int> UpdateTaskScheduler(TaskSchedulerDto taskSchedulerDto);
Task<int> DeleteTaskScheduler(Guid id);
}

View File

@@ -1,15 +1,19 @@
using AutoMapper; using AutoMapper;
using AzureDataModel.Dtos;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TaskScheduler = AzureDataModel.Entities.TaskScheduler; using OrdersManagementDataModel.Dtos;
using OrdersManagementDataModel.Queries;
using TaskScheduler = OrdersManagementDataModel.Entities.TaskScheduler;
namespace AzureDataModel.Services; namespace OrdersManagementDataModel.Services;
public class TaskSchedulerService(AzureDbContext context, IMapper mapper) : ITaskSchedulerService public class TaskSchedulerService(OrdersManagementDbContext context, IMapper mapper) : ITaskSchedulerService
{ {
public async Task<IEnumerable<TaskSchedulerDto>> GetTaskSchedulers() public async Task<IEnumerable<TaskSchedulerDto>> GetTaskSchedulers()
{ {
return await context.TaskSchedulers.Select(x => mapper.Map<TaskSchedulerDto>(x)).ToListAsync(); List<TaskSchedulerDto> taskSchedulers =
(await Task.FromResult(OrdersManagementQueries.GetSchedulers(context))).ToList();
return taskSchedulers;
} }
public async Task<TaskSchedulerDto?> GetTaskSchedulerById(Guid id) public async Task<TaskSchedulerDto?> GetTaskSchedulerById(Guid id)

View File

@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"OrdersManagementConnection": "\"OrdersManagementConnection\": \"Server=192.168.0.7;Database=OrdersManagement;User Id=sa;Password=Tetum#2021!;TrustServerCertificate=true\";"
}
}

View File

@@ -79,6 +79,25 @@ namespace SytelineSaAppEfDataModel.Services
return ediCustomerOrder; return ediCustomerOrder;
} }
public async Task<IEnumerable<EdiCustomerOrderTranslateDto>> FindMissingOrders(DateTime startDate)
{
IList<EdiCustomerOrderTranslate> ediCustomerOrderTranslates = await context.EdiCustomerOrderTranslates
.Where(x => x.CreatedDate >= startDate).ToListAsync();
var ediOrderNumbers = ediCustomerOrderTranslates.Select(x => x.EdiCoCoNum).ToList();
var ediOrderNumbersFromEdiCo = (await context.EdiCustomerOrders.ToListAsync())
.Where(x => ediOrderNumbers.Contains(x.CustomerOrderNumber)).Select(x => x.CustomerOrderNumber)
.ToList();
ISet<string> missingNumbers = ediOrderNumbers.Except(ediOrderNumbersFromEdiCo).ToHashSet();
IEnumerable<EdiCustomerOrderTranslateDto> missingOrders =
(await context.EdiCustomerOrderTranslates.ToListAsync())
.Where(x => missingNumbers.Contains(x.EdiCoCoNum)).Select(mapper.Map<EdiCustomerOrderTranslateDto>);
return missingOrders;
}
public async Task<int> SendOrderToSyteline(Guid customerOrderNumber) public async Task<int> SendOrderToSyteline(Guid customerOrderNumber)
{ {
EdiCustomerOrder? ediCustomerOrder = await context.EdiCustomerOrders.FirstOrDefaultAsync(x => x.RowPointer == customerOrderNumber); EdiCustomerOrder? ediCustomerOrder = await context.EdiCustomerOrders.FirstOrDefaultAsync(x => x.RowPointer == customerOrderNumber);

View File

@@ -12,6 +12,7 @@ namespace SytelineSaAppEfDataModel.Services
Task<IEnumerable<EdiCustomerOrderDto>> GetAll(); Task<IEnumerable<EdiCustomerOrderDto>> GetAll();
Task<IEnumerable<EdiCustomerOrderDto?>> GetByDate(DateTime date); Task<IEnumerable<EdiCustomerOrderDto?>> GetByDate(DateTime date);
Task<EdiCustomerOrderDto?> GetByOrderNumber(Guid orderNumber); Task<EdiCustomerOrderDto?> GetByOrderNumber(Guid orderNumber);
Task<IEnumerable<EdiCustomerOrderTranslateDto>> FindMissingOrders(DateTime startDate);
Task<int> SendOrderToSyteline(Guid customerOrderNumber); Task<int> SendOrderToSyteline(Guid customerOrderNumber);
} }
} }