Convert Trafag exporter to Blazor Server app with UI and scheduler
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class CryptoService
|
||||
{
|
||||
public string Encrypt(string plainText)
|
||||
{
|
||||
var input = Encoding.UTF8.GetBytes(plainText ?? string.Empty);
|
||||
var protectedBytes = ProtectedData.Protect(input, null, DataProtectionScope.CurrentUser);
|
||||
return Convert.ToBase64String(protectedBytes);
|
||||
}
|
||||
|
||||
public string Decrypt(string cipherText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cipherText))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var input = Convert.FromBase64String(cipherText);
|
||||
var unprotectedBytes = ProtectedData.Unprotect(input, null, DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(unprotectedBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using ClosedXML.Excel;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ExcelExportService
|
||||
{
|
||||
public string CreateFile(string baseDirectory, string land, string tsc, List<SalesRecord> records)
|
||||
{
|
||||
var outputDirectory = Path.Combine(baseDirectory, "exports", land);
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
|
||||
var fileName = $"Sales_{tsc}_{DateTime.UtcNow:yyyy-MM-dd}.xlsx";
|
||||
var filePath = Path.Combine(outputDirectory, fileName);
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
var ws = workbook.AddWorksheet("Sales");
|
||||
|
||||
var headers = new[]
|
||||
{
|
||||
"extraction date",
|
||||
"TSC",
|
||||
"Invoice Number",
|
||||
"Position on invoice",
|
||||
"Material",
|
||||
"Name",
|
||||
"Product Group",
|
||||
"Quantity",
|
||||
"Supplier number",
|
||||
"Supplier name",
|
||||
"Supplier country",
|
||||
"Customer number",
|
||||
"Customer name",
|
||||
"Customer country",
|
||||
"Customer Industry",
|
||||
"Standard cost",
|
||||
"Standard Cost Currency",
|
||||
"Purchase Order number",
|
||||
"Sales Price/Value",
|
||||
"Sales Currency",
|
||||
"Incoterms 2020",
|
||||
"Sales responsible employee",
|
||||
"invoice date",
|
||||
"order date",
|
||||
"Land",
|
||||
"Document Type"
|
||||
};
|
||||
|
||||
for (var i = 0; i < headers.Length; i++)
|
||||
{
|
||||
ws.Cell(1, i + 1).Value = headers[i];
|
||||
ws.Cell(1, i + 1).Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
var row = 2;
|
||||
foreach (var r in records)
|
||||
{
|
||||
ws.Cell(row, 1).Value = r.ExtractionDate.ToString("dd.MM.yyyy HH:mm:ss");
|
||||
ws.Cell(row, 2).Value = r.TSC;
|
||||
ws.Cell(row, 3).Value = r.InvoiceNumber;
|
||||
ws.Cell(row, 4).Value = r.PositionOnInvoice;
|
||||
ws.Cell(row, 5).Value = r.Material;
|
||||
ws.Cell(row, 6).Value = r.Name;
|
||||
ws.Cell(row, 7).Value = r.ProductGroup;
|
||||
ws.Cell(row, 8).Value = r.Quantity;
|
||||
ws.Cell(row, 9).Value = r.SupplierNumber;
|
||||
ws.Cell(row, 10).Value = r.SupplierName;
|
||||
ws.Cell(row, 11).Value = r.SupplierCountry;
|
||||
ws.Cell(row, 12).Value = r.CustomerNumber;
|
||||
ws.Cell(row, 13).Value = r.CustomerName;
|
||||
ws.Cell(row, 14).Value = r.CustomerCountry;
|
||||
ws.Cell(row, 15).Value = r.CustomerIndustry;
|
||||
ws.Cell(row, 16).Value = r.StandardCost;
|
||||
ws.Cell(row, 17).Value = r.StandardCostCurrency;
|
||||
ws.Cell(row, 18).Value = r.PurchaseOrderNumber;
|
||||
ws.Cell(row, 19).Value = r.SalesPriceValue;
|
||||
ws.Cell(row, 20).Value = r.SalesCurrency;
|
||||
ws.Cell(row, 21).Value = r.Incoterms2020;
|
||||
ws.Cell(row, 22).Value = r.SalesResponsibleEmployee;
|
||||
ws.Cell(row, 23).Value = r.InvoiceDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 24).Value = r.OrderDate?.ToString("dd.MM.yyyy") ?? string.Empty;
|
||||
ws.Cell(row, 25).Value = r.Land;
|
||||
ws.Cell(row, 26).Value = r.DocumentType;
|
||||
row++;
|
||||
}
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
workbook.SaveAs(filePath);
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class ExportOrchestrationService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
CryptoService cryptoService,
|
||||
HanaQueryService hanaQueryService,
|
||||
ExcelExportService excelExportService,
|
||||
SharePointUploadService sharePointUploadService)
|
||||
{
|
||||
public async Task ExportAllActiveSitesAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var siteIds = await db.Sites.Where(x => x.IsActive).Select(x => x.Id).ToListAsync(ct);
|
||||
|
||||
foreach (var siteId in siteIds)
|
||||
{
|
||||
await ExportSiteAsync(siteId, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExportSiteAsync(int siteId, CancellationToken ct = default)
|
||||
{
|
||||
var started = DateTime.UtcNow;
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var site = await db.Sites.Include(x => x.HanaServer).SingleAsync(x => x.Id == siteId, ct);
|
||||
var settings = await db.ExportSettings.OrderBy(x => x.Id).FirstAsync(ct);
|
||||
var sp = await db.SharePointConfigs.OrderBy(x => x.Id).FirstAsync(ct);
|
||||
|
||||
var log = new ExportLog
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
SiteId = site.Id,
|
||||
Land = site.Land,
|
||||
TSC = site.TSC,
|
||||
Status = "Error",
|
||||
RowCount = 0,
|
||||
FileName = string.Empty,
|
||||
DurationSeconds = 0
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var hanaServer = site.HanaServer ?? throw new InvalidOperationException("HANA Server fehlt.");
|
||||
var hanaPassword = cryptoService.Decrypt(hanaServer.EncryptedPassword);
|
||||
var clientSecret = cryptoService.Decrypt(sp.EncryptedClientSecret);
|
||||
|
||||
var records = hanaQueryService.QuerySales(
|
||||
hanaServer.Host,
|
||||
hanaServer.Port,
|
||||
hanaServer.Username,
|
||||
hanaPassword,
|
||||
site.Schema,
|
||||
site.TSC,
|
||||
site.Land,
|
||||
settings.DateFilter);
|
||||
|
||||
var filePath = excelExportService.CreateFile(AppContext.BaseDirectory, site.Land, site.TSC, records);
|
||||
|
||||
await sharePointUploadService.UploadAsync(
|
||||
sp.SiteUrl,
|
||||
sp.ExportFolder,
|
||||
sp.TenantId,
|
||||
sp.ClientId,
|
||||
clientSecret,
|
||||
site.Land,
|
||||
filePath);
|
||||
|
||||
log.Status = "OK";
|
||||
log.RowCount = records.Count;
|
||||
log.FileName = Path.GetFileName(filePath);
|
||||
log.ErrorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.ErrorMessage = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
log.DurationSeconds = (DateTime.UtcNow - started).TotalSeconds;
|
||||
db.ExportLogs.Add(log);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DateTime?> GetNextRunAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var settings = await db.ExportSettings.OrderBy(x => x.Id).FirstOrDefaultAsync(ct);
|
||||
if (settings is null || !settings.TimerEnabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var next = new DateTime(now.Year, now.Month, now.Day, settings.TimerHour, settings.TimerMinute, 0);
|
||||
if (next <= now)
|
||||
{
|
||||
next = next.AddDays(1);
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<int, ExportLog?>> GetLatestLogsPerSiteAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(ct);
|
||||
var grouped = await db.ExportLogs
|
||||
.OrderByDescending(x => x.Timestamp)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return grouped
|
||||
.GroupBy(x => x.SiteId ?? 0)
|
||||
.ToDictionary(g => g.Key, g => g.FirstOrDefault());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Sap.Data.Hana;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class HanaQueryService
|
||||
{
|
||||
public List<SalesRecord> QuerySales(string host, int port, string username, string password, string schema, string tsc, string land, string dateFilter)
|
||||
{
|
||||
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}";
|
||||
var result = new List<SalesRecord>();
|
||||
|
||||
using var connection = new HanaConnection(connectionString);
|
||||
connection.Open();
|
||||
|
||||
var invoiceQuery = GetInvoiceQuery(schema, tsc, dateFilter);
|
||||
var creditQuery = GetCreditNoteQuery(schema, tsc, dateFilter);
|
||||
|
||||
result.AddRange(Read(connection, invoiceQuery, land));
|
||||
result.AddRange(Read(connection, creditQuery, land));
|
||||
|
||||
foreach (var record in result)
|
||||
{
|
||||
if (record.Material.Contains('/'))
|
||||
{
|
||||
var parts = record.Material.Split('/');
|
||||
record.Material = parts[^1];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TestConnection(string host, int port, string username, string password)
|
||||
{
|
||||
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}";
|
||||
using var connection = new HanaConnection(connectionString);
|
||||
connection.Open();
|
||||
return connection.State == System.Data.ConnectionState.Open;
|
||||
}
|
||||
|
||||
private static List<SalesRecord> Read(HanaConnection connection, string query, string land)
|
||||
{
|
||||
var records = new List<SalesRecord>();
|
||||
using var cmd = new HanaCommand(query, connection);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
records.Add(new SalesRecord
|
||||
{
|
||||
ExtractionDate = reader.GetDateTime(reader.GetOrdinal("extraction_date")),
|
||||
TSC = reader["tsc"]?.ToString() ?? string.Empty,
|
||||
InvoiceNumber = reader["invoice_number"]?.ToString() ?? string.Empty,
|
||||
PositionOnInvoice = Convert.ToInt32(reader["invoice_position"]),
|
||||
InvoiceDate = reader.IsDBNull(reader.GetOrdinal("invoice_date")) ? null : reader.GetDateTime(reader.GetOrdinal("invoice_date")),
|
||||
Material = reader["material"]?.ToString() ?? string.Empty,
|
||||
Name = reader["material_name"]?.ToString() ?? string.Empty,
|
||||
ProductGroup = reader["product_group"]?.ToString() ?? string.Empty,
|
||||
Quantity = Convert.ToDecimal(reader["quantity"]),
|
||||
SupplierNumber = reader["supplier_number"]?.ToString() ?? string.Empty,
|
||||
SupplierName = reader["supplier_name"]?.ToString() ?? string.Empty,
|
||||
SupplierCountry = reader["supplier_country"]?.ToString() ?? string.Empty,
|
||||
CustomerNumber = reader["customer_number"]?.ToString() ?? string.Empty,
|
||||
CustomerName = reader["customer_name"]?.ToString() ?? string.Empty,
|
||||
CustomerCountry = reader["customer_country"]?.ToString() ?? string.Empty,
|
||||
CustomerIndustry = reader["customer_industry"]?.ToString() ?? string.Empty,
|
||||
StandardCost = Convert.ToDecimal(reader["standard_cost"]),
|
||||
StandardCostCurrency = reader["standard_cost_currency"]?.ToString() ?? string.Empty,
|
||||
PurchaseOrderNumber = reader["purchase_order_number"]?.ToString() ?? string.Empty,
|
||||
SalesPriceValue = Convert.ToDecimal(reader["sales_value"]),
|
||||
SalesCurrency = reader["sales_currency"]?.ToString() ?? string.Empty,
|
||||
Incoterms2020 = reader["incoterms_2020"]?.ToString() ?? string.Empty,
|
||||
SalesResponsibleEmployee = reader["sales_responsible"]?.ToString() ?? string.Empty,
|
||||
OrderDate = reader.IsDBNull(reader.GetOrdinal("order_date")) ? null : reader.GetDateTime(reader.GetOrdinal("order_date")),
|
||||
Land = land,
|
||||
DocumentType = reader["doc_type"]?.ToString() ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private static string GetInvoiceQuery(string schema, string tsc, string dateFilter) => $@"
|
||||
SELECT
|
||||
CURRENT_TIMESTAMP AS extraction_date,
|
||||
'{tsc}' AS tsc,
|
||||
h.""DocNum"" AS invoice_number,
|
||||
p.""LineNum"" AS invoice_position,
|
||||
h.""DocDate"" AS invoice_date,
|
||||
p.""ItemCode"" AS material,
|
||||
p.""Dscription"" AS material_name,
|
||||
COALESCE(grp.""ItmsGrpNam"", '') AS product_group,
|
||||
p.""Quantity"" AS quantity,
|
||||
COALESCE(itm.""CardCode"", '') AS supplier_number,
|
||||
COALESCE(sup.""CardName"", '') AS supplier_name,
|
||||
COALESCE(sup_adr.""Country"", '') AS supplier_country,
|
||||
h.""CardCode"" AS customer_number,
|
||||
h.""CardName"" AS customer_name,
|
||||
COALESCE(cust_adr.""Country"", '') AS customer_country,
|
||||
COALESCE(ind.""IndName"", '') AS customer_industry,
|
||||
p.""StockPrice"" AS standard_cost,
|
||||
COALESCE(p.""Currency"", h.""DocCur"") AS standard_cost_currency,
|
||||
CASE WHEN p.""BaseType"" = 22
|
||||
THEN CAST(p.""BaseRef"" AS NVARCHAR(20))
|
||||
ELSE '' END AS purchase_order_number,
|
||||
p.""LineTotal"" AS sales_value,
|
||||
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
||||
'' AS incoterms_2020,
|
||||
COALESCE(emp.""SlpName"", '') AS sales_responsible,
|
||||
CASE WHEN p.""BaseType"" = 17
|
||||
THEN (SELECT o.""DocDate"" FROM {schema}.""ORDR"" o
|
||||
WHERE o.""DocEntry"" = p.""BaseEntry"")
|
||||
ELSE NULL END AS order_date,
|
||||
'INV' AS doc_type
|
||||
FROM {schema}.""OINV"" h
|
||||
INNER JOIN {schema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry""
|
||||
LEFT JOIN {schema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
|
||||
LEFT JOIN {schema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
|
||||
LEFT JOIN {schema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
|
||||
LEFT JOIN {schema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode""
|
||||
AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode""
|
||||
LEFT JOIN {schema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode""
|
||||
LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
|
||||
AND sup.""CardType"" = 'S'
|
||||
LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
|
||||
AND sup_adr.""AdresType"" = 'B'
|
||||
LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
|
||||
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
|
||||
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
|
||||
|
||||
private static string GetCreditNoteQuery(string schema, string tsc, string dateFilter) => $@"
|
||||
SELECT
|
||||
CURRENT_TIMESTAMP AS extraction_date,
|
||||
'{tsc}' AS tsc,
|
||||
h.""DocNum"" AS invoice_number,
|
||||
p.""LineNum"" AS invoice_position,
|
||||
h.""DocDate"" AS invoice_date,
|
||||
p.""ItemCode"" AS material,
|
||||
p.""Dscription"" AS material_name,
|
||||
COALESCE(grp.""ItmsGrpNam"", '') AS product_group,
|
||||
p.""Quantity"" * -1 AS quantity,
|
||||
COALESCE(itm.""CardCode"", '') AS supplier_number,
|
||||
COALESCE(sup.""CardName"", '') AS supplier_name,
|
||||
COALESCE(sup_adr.""Country"", '') AS supplier_country,
|
||||
h.""CardCode"" AS customer_number,
|
||||
h.""CardName"" AS customer_name,
|
||||
COALESCE(cust_adr.""Country"", '') AS customer_country,
|
||||
COALESCE(ind.""IndName"", '') AS customer_industry,
|
||||
p.""StockPrice"" AS standard_cost,
|
||||
COALESCE(p.""Currency"", h.""DocCur"") AS standard_cost_currency,
|
||||
'' AS purchase_order_number,
|
||||
p.""LineTotal"" * -1 AS sales_value,
|
||||
COALESCE(p.""Currency"", h.""DocCur"") AS sales_currency,
|
||||
'' AS incoterms_2020,
|
||||
COALESCE(emp.""SlpName"", '') AS sales_responsible,
|
||||
NULL AS order_date,
|
||||
'CRN' AS doc_type
|
||||
FROM {schema}.""ORIN"" h
|
||||
INNER JOIN {schema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry""
|
||||
LEFT JOIN {schema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode""
|
||||
LEFT JOIN {schema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod""
|
||||
LEFT JOIN {schema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode""
|
||||
LEFT JOIN {schema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode""
|
||||
AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode""
|
||||
LEFT JOIN {schema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode""
|
||||
LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
|
||||
AND sup.""CardType"" = 'S'
|
||||
LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
|
||||
AND sup_adr.""AdresType"" = 'B'
|
||||
LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
|
||||
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
|
||||
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Azure.Identity;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Graph.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class SharePointUploadService
|
||||
{
|
||||
public async Task UploadAsync(string siteUrl, string exportFolder, string tenantId, string clientId, string clientSecret, string land, string localFilePath)
|
||||
{
|
||||
var graph = CreateGraphClient(tenantId, clientId, clientSecret);
|
||||
var (siteId, driveId) = await ResolveSiteAndDriveAsync(graph, siteUrl);
|
||||
|
||||
var folderPath = $"{exportFolder.Trim('/')}/{land}";
|
||||
await EnsureFolderPathAsync(graph, driveId, folderPath);
|
||||
|
||||
var fileName = Path.GetFileName(localFilePath);
|
||||
var remotePath = $"{folderPath}/{fileName}";
|
||||
|
||||
await using var stream = File.OpenRead(localFilePath);
|
||||
await graph.Drives[driveId].Root.ItemWithPath(remotePath).Content.PutAsync(stream);
|
||||
}
|
||||
|
||||
public async Task<bool> TestConnectionAsync(string siteUrl, string tenantId, string clientId, string clientSecret)
|
||||
{
|
||||
var graph = CreateGraphClient(tenantId, clientId, clientSecret);
|
||||
var (siteId, _) = await ResolveSiteAndDriveAsync(graph, siteUrl);
|
||||
return !string.IsNullOrWhiteSpace(siteId);
|
||||
}
|
||||
|
||||
private static GraphServiceClient CreateGraphClient(string tenantId, string clientId, string clientSecret)
|
||||
{
|
||||
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||
return new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
|
||||
}
|
||||
|
||||
private static async Task<(string siteId, string driveId)> ResolveSiteAndDriveAsync(GraphServiceClient graph, string siteUrl)
|
||||
{
|
||||
var uri = new Uri(siteUrl);
|
||||
var site = await graph.Sites[$"{uri.Host}:{uri.AbsolutePath}"].GetAsync();
|
||||
if (site?.Id is null)
|
||||
{
|
||||
throw new InvalidOperationException("SharePoint Site nicht gefunden.");
|
||||
}
|
||||
|
||||
var drive = await graph.Sites[site.Id].Drive.GetAsync();
|
||||
if (drive?.Id is null)
|
||||
{
|
||||
throw new InvalidOperationException("SharePoint Dokumentenbibliothek nicht gefunden.");
|
||||
}
|
||||
|
||||
return (site.Id, drive.Id);
|
||||
}
|
||||
|
||||
private static async Task EnsureFolderPathAsync(GraphServiceClient graph, string driveId, string folderPath)
|
||||
{
|
||||
var segments = folderPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
var currentPath = string.Empty;
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
currentPath = string.IsNullOrEmpty(currentPath) ? segment : $"{currentPath}/{segment}";
|
||||
|
||||
try
|
||||
{
|
||||
_ = await graph.Drives[driveId].Root.ItemWithPath(currentPath).GetAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
var parentPath = currentPath.Contains('/')
|
||||
? currentPath[..currentPath.LastIndexOf('/')]
|
||||
: string.Empty;
|
||||
|
||||
var parent = string.IsNullOrEmpty(parentPath)
|
||||
? await graph.Drives[driveId].Root.GetAsync()
|
||||
: await graph.Drives[driveId].Root.ItemWithPath(parentPath).GetAsync();
|
||||
|
||||
if (parent?.Id is null)
|
||||
{
|
||||
throw new InvalidOperationException("SharePoint Parent-Ordner konnte nicht ermittelt werden.");
|
||||
}
|
||||
|
||||
await graph.Drives[driveId].Items[parent.Id].Children.PostAsync(new DriveItem
|
||||
{
|
||||
Name = segment,
|
||||
Folder = new Folder(),
|
||||
AdditionalData = new Dictionary<string, object>
|
||||
{
|
||||
["@microsoft.graph.conflictBehavior"] = "replace"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class TimerBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<TimerBackgroundService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
|
||||
var exportService = scope.ServiceProvider.GetRequiredService<ExportOrchestrationService>();
|
||||
|
||||
await using var db = await dbFactory.CreateDbContextAsync(stoppingToken);
|
||||
var settings = await db.ExportSettings.OrderBy(x => x.Id).FirstOrDefaultAsync(stoppingToken);
|
||||
|
||||
if (settings is null || !settings.TimerEnabled)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var nextRun = new DateTime(now.Year, now.Month, now.Day, settings.TimerHour, settings.TimerMinute, 0);
|
||||
if (nextRun <= now)
|
||||
{
|
||||
nextRun = nextRun.AddDays(1);
|
||||
}
|
||||
|
||||
var delay = nextRun - now;
|
||||
logger.LogInformation("Nächster automatischer Export um {NextRun}", nextRun);
|
||||
await Task.Delay(delay, stoppingToken);
|
||||
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await exportService.ExportAllActiveSitesAsync(stoppingToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Fehler im TimerBackgroundService");
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user