Complete DI migration: wire all business services end-to-end

Move the intranet off the static-helper / Active-Record pattern onto
constructor-injected services, removing controller coupling and the
sync-over-async (Task.Run().Wait()) hot spots in the data classes.

Services now registered and consumed via DI:
- IBankingService, IPdfService, IMfrClientFactory (singletons)
- IWidgetService, IReportService, IInvoiceService, IReminderService (scoped)

Key changes:
- FuchsWidgetService: real widget logic (sql_table/indicator/html +
  rendering_options) ported from the static class, which is deleted.
- FuchsReportService + FuchsVisualization: report engine decoupled from
  IntranetController (takes connStr/dbSec/userAccountId); static
  FuchsReports deleted.
- InvoiceService / ReminderService: implement load/register/render/store
  (previously NotImplementedException stubs). FdsInvoiceData /
  FdsReminderData are now pure data holders — all DB + PDF work moved into
  the services, async throughout (no Task.Run().Wait()).
- Controllers inject and call the services; all `new FdsMfrClient()` calls
  go through IMfrClientFactory.
- Deleted dead code: static Banking, FuchsWidgets, FuchsReports, and the
  unused IDbConnectionFactory.
- InternalsVisibleTo("Fuchs.Tests") for testing internal mapping logic.

Tests: 127 passing (Banking tests moved to the service; added data-holder
tests for FdsInvoiceData/FdsReminderData). Full solution builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 12:57:59 +02:00
parent c81619fa53
commit 8dee630abb
25 changed files with 711 additions and 947 deletions
+19 -14
View File
@@ -1,17 +1,20 @@
using System.Data; using System.Data;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Fuchs.intranet; using Fuchs.Services;
using Microsoft.Extensions.Logging.Abstractions;
using programmersdigest.MT940Parser; using programmersdigest.MT940Parser;
using Xunit; using Xunit;
namespace Fuchs.Tests; namespace Fuchs.Tests;
/// <summary> /// <summary>
/// Banking helper robustness tests. /// Banking service robustness tests.
/// </summary> /// </summary>
public class BankingDebitCreditMarkTests public class BankingDebitCreditMarkTests
{ {
private static readonly BankingService Svc = new(NullLogger<BankingService>.Instance);
[Theory] [Theory]
[InlineData(DebitCreditMark.Credit, "C")] [InlineData(DebitCreditMark.Credit, "C")]
[InlineData(DebitCreditMark.Debit, "D")] [InlineData(DebitCreditMark.Debit, "D")]
@@ -19,18 +22,20 @@ public class BankingDebitCreditMarkTests
[InlineData(DebitCreditMark.ReverseDebit, "RD")] [InlineData(DebitCreditMark.ReverseDebit, "RD")]
public void DebitCreditMarkAbb_ReturnsExpected(DebitCreditMark mark, string expected) public void DebitCreditMarkAbb_ReturnsExpected(DebitCreditMark mark, string expected)
{ {
Assert.Equal(expected, Banking.DebitCreditMarkAbb(mark)); Assert.Equal(expected, Svc.DebitCreditMarkAbb(mark));
} }
[Fact] [Fact]
public void DebitCreditMarkAbb_UndefinedValue_ReturnsEmpty() public void DebitCreditMarkAbb_UndefinedValue_ReturnsEmpty()
{ {
Assert.Equal("", Banking.DebitCreditMarkAbb((DebitCreditMark)999)); Assert.Equal("", Svc.DebitCreditMarkAbb((DebitCreditMark)999));
} }
} }
public class BankingParseToDatatableTests public class BankingParseToDatatableTests
{ {
private static readonly BankingService Svc = new(NullLogger<BankingService>.Instance);
private static readonly string MinimalMT940 = private static readonly string MinimalMT940 =
"\r\n:20:STARTUMSE\r\n" + "\r\n:20:STARTUMSE\r\n" +
":25:DE12345678901234567890\r\n" + ":25:DE12345678901234567890\r\n" +
@@ -48,7 +53,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_ReturnsOneRow() public void ParseToDatatable_ValidMT940_ReturnsOneRow()
{ {
using var stream = ToStream(MinimalMT940); using var stream = ToStream(MinimalMT940);
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.Equal(1, table.Rows.Count); Assert.Equal(1, table.Rows.Count);
} }
@@ -56,7 +61,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_HasAccountColumn() public void ParseToDatatable_ValidMT940_HasAccountColumn()
{ {
using var stream = ToStream(MinimalMT940); using var stream = ToStream(MinimalMT940);
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.True(table.Columns.Contains("AccountIdentification")); Assert.True(table.Columns.Contains("AccountIdentification"));
Assert.Equal("DE12345678901234567890", table.Rows[0]["AccountIdentification"]); Assert.Equal("DE12345678901234567890", table.Rows[0]["AccountIdentification"]);
} }
@@ -65,7 +70,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_HasAmountColumn() public void ParseToDatatable_ValidMT940_HasAmountColumn()
{ {
using var stream = ToStream(MinimalMT940); using var stream = ToStream(MinimalMT940);
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.True(table.Columns.Contains("Amount")); Assert.True(table.Columns.Contains("Amount"));
Assert.Equal(500m, table.Rows[0]["Amount"]); Assert.Equal(500m, table.Rows[0]["Amount"]);
} }
@@ -74,7 +79,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_ValidMT940_HasDebitCreditMark() public void ParseToDatatable_ValidMT940_HasDebitCreditMark()
{ {
using var stream = ToStream(MinimalMT940); using var stream = ToStream(MinimalMT940);
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.Equal("C", table.Rows[0]["DebitCreditMark"]); Assert.Equal("C", table.Rows[0]["DebitCreditMark"]);
} }
@@ -82,7 +87,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_EmptyStream_ReturnsEmptyTable() public void ParseToDatatable_EmptyStream_ReturnsEmptyTable()
{ {
using var stream = ToStream(""); using var stream = ToStream("");
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.Equal(0, table.Rows.Count); Assert.Equal(0, table.Rows.Count);
} }
@@ -90,7 +95,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_EmptyStream_HasDefaultSchema() public void ParseToDatatable_EmptyStream_HasDefaultSchema()
{ {
using var stream = ToStream(""); using var stream = ToStream("");
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.True(table.Columns.Contains("AccountIdentification")); Assert.True(table.Columns.Contains("AccountIdentification"));
Assert.True(table.Columns.Contains("Amount")); Assert.True(table.Columns.Contains("Amount"));
Assert.True(table.Columns.Contains("DebitCreditMark")); Assert.True(table.Columns.Contains("DebitCreditMark"));
@@ -104,7 +109,7 @@ public class BankingParseToDatatableTests
schema.Columns.Add("Amount", typeof(decimal)); schema.Columns.Add("Amount", typeof(decimal));
using var stream = ToStream(MinimalMT940); using var stream = ToStream(MinimalMT940);
var table = Banking.ParseToDatatable(stream, schemaDatatable: schema); var table = Svc.ParseToDatatable(stream, schemaDatatable: schema);
Assert.Equal(2, table.Columns.Count); Assert.Equal(2, table.Columns.Count);
} }
@@ -113,7 +118,7 @@ public class BankingParseToDatatableTests
{ {
var multi = MinimalMT940 + "\n" + MinimalMT940; var multi = MinimalMT940 + "\n" + MinimalMT940;
using var stream = ToStream(multi); using var stream = ToStream(multi);
var table = Banking.ParseToDatatable(stream); var table = Svc.ParseToDatatable(stream);
Assert.Equal(2, table.Rows.Count); Assert.Equal(2, table.Rows.Count);
} }
@@ -121,7 +126,7 @@ public class BankingParseToDatatableTests
public void ParseToDatatable_MalformedContent_DoesNotThrow() public void ParseToDatatable_MalformedContent_DoesNotThrow()
{ {
using var stream = ToStream("This is not MT940 data at all"); using var stream = ToStream("This is not MT940 data at all");
var ex = Record.Exception(() => Banking.ParseToDatatable(stream)); var ex = Record.Exception(() => Svc.ParseToDatatable(stream));
Assert.Null(ex); Assert.Null(ex);
} }
} }
+83
View File
@@ -0,0 +1,83 @@
using Fuchs.intranet;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace Fuchs.Tests;
/// <summary>
/// Tests for the refactored data holders (FdsInvoiceData / FdsReminderData).
/// These are now pure data objects — persistence/PDF moved to the DI services.
/// </summary>
public class FdsDataTests
{
[Fact]
public void FdsInvoiceData_Empty_DefaultsToDraftWithNoRegistration()
{
var inv = new FdsInvoiceData();
Assert.True(inv.IsDraft);
Assert.Null(inv.InvoiceRegistration);
Assert.Equal("", inv.Id);
Assert.Equal("R", inv.InvoiceType); // fallback when no registration
}
[Fact]
public void FdsInvoiceData_ParsesFormSections()
{
var jo = JObject.Parse(
"{ \"admin\": { \"type\": \"R\", \"customerid\": \"42\" }, " +
" \"new\": { \"title\": \"Test\", \"total_gross\": \"119.00\" }, " +
" \"sms\": { \"tscn\": \"10\" }, " +
" \"req\": [] }");
var inv = new FdsInvoiceData(jo);
Assert.NotNull(inv.Admin);
Assert.NotNull(inv.NewValues);
Assert.NotNull(inv.Sms);
Assert.Equal("R", inv.Admin!.getString("type"));
Assert.Equal("Test", inv.NewValues!.getString("title"));
Assert.True(inv.IsDraft);
}
[Fact]
public void FdsInvoiceData_BuildInvoiceParams_PicksHighestVatAndMapsFields()
{
// two line items with different VAT rates → highest (19) selected
var jo = JObject.Parse(
"{ \"admin\": { \"type\": \"R\", \"customerid\": \"7\", \"p13b\": false }, " +
" \"new\": { \"title\": \"Bad\", \"total_gross\": \"119\", \"total_net\": \"100\", \"vat_19_net\": \"100\", \"paymentterm\": \"10d\" }, " +
" \"req\": [ { \"items\": [ { \"vat\": \"7%\" }, { \"vat\": \"19%\" } ] } ] }");
var inv = new FdsInvoiceData(jo);
var pl = inv.BuildInvoiceParams(change: false, invId: "");
var vat = pl.Find(p => p.ParameterName == "@InvoiceVAT_1");
var title = pl.Find(p => p.ParameterName == "@InvoiceTitle");
Assert.NotNull(vat);
Assert.Equal("19", vat!.Value);
Assert.Equal("Bad", title!.Value);
}
[Fact]
public void FdsReminderData_Empty_DefaultsToDraft()
{
var rem = new FdsReminderData();
Assert.True(rem.IsDraft);
Assert.Null(rem.ReminderRegistration);
Assert.Equal("", rem.Id);
}
[Fact]
public void FdsReminderData_ParsesFormSections()
{
var jo = JObject.Parse(
"{ \"new\": { \"amount\": \"50\", \"invoiceemail\": \"a@b.de\" }, " +
" \"rem\": { \"type\": \"f\", \"invid\": \"123\" } }");
var rem = new FdsReminderData(jo);
Assert.NotNull(rem.NewValues);
Assert.NotNull(rem.Rem);
Assert.Equal("50", rem.NewValues!.getString("amount"));
Assert.Equal("f", rem.Rem!.getString("type"));
}
}
@@ -29,7 +29,7 @@ public partial class IntranetController
_intranet.Intranet__SQLConnectionString, _intranet.Intranet__SQLConnectionString,
Security: DbSec, options: SqlOpt(fn, id, code))).DataTable; Security: DbSec, options: SqlOpt(fn, id, code))).DataTable;
var tbl = Banking.ParseToDatatable(stream, schemaDt); var tbl = _banking.ParseToDatatable(stream, schemaDt);
var tmptbl = "bs_" + Guid.NewGuid().ToString().Replace("-", ""); var tmptbl = "bs_" + Guid.NewGuid().ToString().Replace("-", "");
var dtwa = new DatatableWriterAsync(tbl, _intranet.Intranet__SQLConnectionString) var dtwa = new DatatableWriterAsync(tbl, _intranet.Intranet__SQLConnectionString)
@@ -146,7 +146,7 @@ public partial class IntranetController
return BadRequest400(); return BadRequest400();
} }
_logger.LogInformation("mfrrel: resetting MFR relation for invoice {InvoiceId}, user={User}", relId, UserAccountID); _logger.LogInformation("mfrrel: resetting MFR relation for invoice {InvoiceId}, user={User}", relId, UserAccountID);
using (var mfr = new fds.FdsMfrClient()) using (var mfr = _mfrFactory.Create())
await mfr.Update__entitytable(EntityTypes.Invoice, await mfr.Update__entitytable(EntityTypes.Invoice,
fds.FdsMfr.UpdateNeed.Reset, new[] { relId }); fds.FdsMfr.UpdateNeed.Reset, new[] { relId });
return Ok(); return Ok();
@@ -22,7 +22,7 @@ public partial class IntranetController
if (!long.TryParse(Form("id"), out long tgtid)) { _logger.LogWarning("HandleInvoicePget: invalid 'id' value='{Value}' user={User}", Form("id"), UserAccountID); return BadRequest400(); } if (!long.TryParse(Form("id"), out long tgtid)) { _logger.LogWarning("HandleInvoicePget: invalid 'id' value='{Value}' user={User}", Form("id"), UserAccountID); return BadRequest400(); }
_logger.LogDebug("HandleInvoicePget tgtid={TgtId} user={User}", tgtid, UserAccountID); _logger.LogDebug("HandleInvoicePget tgtid={TgtId} user={User}", tgtid, UserAccountID);
using (var mfr = new fds.FdsMfrClient()) using (var mfr = _mfrFactory.Create())
{ {
_logger.LogDebug("HandleInvoicePget resetting invoice entity tgtid={TgtId}", tgtid); _logger.LogDebug("HandleInvoicePget resetting invoice entity tgtid={TgtId}", tgtid);
await mfr.Update__entitytable(EntityTypes.Invoice, await mfr.Update__entitytable(EntityTypes.Invoice,
@@ -50,7 +50,7 @@ public partial class IntranetController
} }
} }
_logger.LogDebug("HandleInvoicePget resetting {InvCount} invoices and {SrqCount} service requests", invIds.Count, srqIds.Count); _logger.LogDebug("HandleInvoicePget resetting {InvCount} invoices and {SrqCount} service requests", invIds.Count, srqIds.Count);
using var mfr2 = new fds.FdsMfrClient(); using var mfr2 = _mfrFactory.Create();
foreach (var iid in invIds) foreach (var iid in invIds)
await mfr2.Update__entitytable(EntityTypes.Invoice, fds.FdsMfr.UpdateNeed.Reset, new[] { iid }); await mfr2.Update__entitytable(EntityTypes.Invoice, fds.FdsMfr.UpdateNeed.Reset, new[] { iid });
foreach (var iid in srqIds) foreach (var iid in srqIds)
@@ -413,9 +413,9 @@ public partial class IntranetController
return ldic; return ldic;
} }
private static async Task<string[]> BuildPdfImageArray(byte[] content) private async Task<string[]> BuildPdfImageArray(byte[] content)
{ {
var imgcol = await FuchsPdf.BytesToImageCollection(content); var imgcol = await _pdf.BytesToImageCollectionAsync(content);
return imgcol.ImgB64Array; return imgcol.ImgB64Array;
} }
} }
@@ -38,11 +38,11 @@ public partial class IntranetController
{ {
if (!HasForm("remc")) return BadRequest400(); if (!HasForm("remc")) return BadRequest400();
var ctd = JsonConvert.DeserializeObject(Form("remc"))!; var ctd = JsonConvert.DeserializeObject(Form("remc"))!;
var fdRem = new FdsReminderData(ctd); var fdRem = await _reminders.RegisterReminderAsync(
fdRem.RegisterReminder(this, change: false, remId: ""); new FdsReminderData(ctd), change: false, remId: "", UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdRem.Id)) if (!string.IsNullOrEmpty(fdRem.Id))
{ {
var imgcol = await FuchsPdf.DocToImageCollection(fdRem.ReminderPDF(this)); var imgcol = await _pdf.DocToImageCollectionAsync(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages }); return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
} }
return StatusCode(500, new { error = "Erinnerung wurde nicht registriert" }); return StatusCode(500, new { error = "Erinnerung wurde nicht registriert" });
@@ -64,12 +64,11 @@ public partial class IntranetController
case "rdoc": case "rdoc":
{ {
if (!HasForm("id")) return BadRequest400(); if (!HasForm("id")) return BadRequest400();
byte[]? fc = null; var (file, fc) = await _reminders.GetStoredFileAsync(Form("id"), UserAccountID, DbSec);
var file = FdsReminderData.GetStoredFile(ref fc, Form("id"), this); if (file == null || fc == null) return StatusCode(404, new { error = "Dokument wurde nicht gefunden" });
if (file == null) return StatusCode(404, new { error = "Dokument wurde nicht gefunden" });
return Form("typ") != "img" return Form("typ") != "img"
? await FileContentResultAsync(fc!, file.MimeType(), file.Name) ? await FileContentResultAsync(fc, file.MimeType(), file.Name)
: await JSONAsync(new { id = Form("id"), img = await BuildPdfImageArray(fc!) }); : await JSONAsync(new { id = Form("id"), img = await BuildPdfImageArray(fc) });
} }
case "idoc": return await HandleReminderIdoc(fn, id, code); case "idoc": return await HandleReminderIdoc(fn, id, code);
@@ -107,8 +106,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true) if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{ {
string remId = frdic["Id"]?.ToString() ?? ""; string remId = frdic["Id"]?.ToString() ?? "";
var fdRem = new FdsReminderData(remId, this); var fdRem = await _reminders.LoadReminderAsync(remId, UserAccountID, DbSec);
byte[] filebyte = await fdRem.StoreReminderDocumentFile(this); byte[] filebyte = await _reminders.StoreReminderDocumentFileAsync(fdRem, fdRem.IsDraft, UserAccountID, DbSec);
string email = frdic.nz("SendToEmail", ""); string email = frdic.nz("SendToEmail", "");
if (!string.IsNullOrEmpty(email) && filebyte.Length > 0) if (!string.IsNullOrEmpty(email) && filebyte.Length > 0)
{ {
@@ -140,17 +139,17 @@ public partial class IntranetController
private async Task<IActionResult> HandleReminderIdoc(string fn, string id, string code) private async Task<IActionResult> HandleReminderIdoc(string fn, string id, string code)
{ {
if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404); if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404);
var fdRem = new FdsReminderData(Form("id"), this); var fdRem = await _reminders.LoadReminderAsync(Form("id"), UserAccountID, DbSec);
if (string.IsNullOrEmpty(fdRem.Id)) return StatusCode(404, new { error = "Erinnerung wurde nicht gefunden" }); if (string.IsNullOrEmpty(fdRem.Id)) return StatusCode(404, new { error = "Erinnerung wurde nicht gefunden" });
string filename = fdRem.ReminderRegistration.nz("DocumentName").ne($"Zahlungserinnerung_{fdRem.Id}.pdf"); string filename = fdRem.ReminderRegistration!.nz("DocumentName").ne($"Zahlungserinnerung_{fdRem.Id}.pdf");
if (Form("typ") != "img") if (Form("typ") != "img")
{ {
byte[] ct = Form("create", "0") != "1" byte[] ct = Form("create", "0") != "1"
? (await fdRem.GetReminderFile(this)) is { Length: > 0 } f1 ? f1 : await fdRem.StoreReminderDocumentFile(this) ? (await _reminders.GetReminderFileAsync(fdRem, fdRem.IsDraft, _mfr, UserAccountID, DbSec)) is { Length: > 0 } f1 ? f1 : await _reminders.StoreReminderDocumentFileAsync(fdRem, fdRem.IsDraft, UserAccountID, DbSec)
: FuchsPdf.DocToPdfBytes(fdRem.ReminderPDF(this)); : _pdf.DocToPdfBytes(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await FileContentResultAsync(ct, "application/pdf", filename, inline: true); return await FileContentResultAsync(ct, "application/pdf", filename, inline: true);
} }
var imgcol = await FuchsPdf.DocToImageCollection(fdRem.ReminderPDF(this)); var imgcol = await _pdf.DocToImageCollectionAsync(_reminders.GenerateReminderPdf(fdRem, fdRem.IsDraft));
return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages }); return await JSONAsync(new { id = fdRem.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
} }
@@ -46,7 +46,7 @@ public partial class IntranetController
} }
default: default:
return await FuchsReports.ProcessFdsRequest(this, id.ToLower(), code); return await _reports.ProcessRequestAsync(id.ToLower(), code, UserAccountID, DbSec, RequestParamsDict());
} }
} }
} }
@@ -45,8 +45,9 @@ public partial class IntranetController
case "save": case "save":
{ {
if (!HasForm("invc")) return BadRequest400(); if (!HasForm("invc")) return BadRequest400();
var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!); var fdInv = await _invoices.RegisterInvoiceAsync(
fdInv.RegisterInvoice(this, change: !string.IsNullOrEmpty(Form("id")), invId: Form("id")); new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
change: !string.IsNullOrEmpty(Form("id")), invId: Form("id"), UserAccountID, DbSec);
return !string.IsNullOrEmpty(fdInv.Id) return !string.IsNullOrEmpty(fdInv.Id)
? await JSONAsync(new { id = fdInv.Id }) ? await JSONAsync(new { id = fdInv.Id })
: StatusCode(500, new { error = "Rechnung wurde nicht gespeichert" }); : StatusCode(500, new { error = "Rechnung wurde nicht gespeichert" });
@@ -55,11 +56,12 @@ public partial class IntranetController
case "sprep": case "sprep":
{ {
if (!HasForm("invc")) return BadRequest400(); if (!HasForm("invc")) return BadRequest400();
var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!); var fdInv = await _invoices.RegisterInvoiceAsync(
fdInv.RegisterInvoice(this, change: false, invId: ""); new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
change: false, invId: "", UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdInv.Id)) if (!string.IsNullOrEmpty(fdInv.Id))
{ {
var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this)); var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages }); return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
} }
return StatusCode(500, new { error = "Rechnung wurde nicht registriert" }); return StatusCode(500, new { error = "Rechnung wurde nicht registriert" });
@@ -68,11 +70,12 @@ public partial class IntranetController
case "sedit": case "sedit":
{ {
if (!HasForm("id", "invc")) return BadRequest400(); if (!HasForm("id", "invc")) return BadRequest400();
var fdInv = new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!); var fdInv = await _invoices.RegisterInvoiceAsync(
fdInv.RegisterInvoice(this, change: true, invId: Form("id")); new FdsInvoiceData(JsonConvert.DeserializeObject(Form("invc"))!),
change: true, invId: Form("id"), UserAccountID, DbSec);
if (!string.IsNullOrEmpty(fdInv.Id)) if (!string.IsNullOrEmpty(fdInv.Id))
{ {
var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this)); var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages }); return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
} }
return StatusCode(500, new { error = "Rechnung wurde nicht registriert" }); return StatusCode(500, new { error = "Rechnung wurde nicht registriert" });
@@ -171,7 +174,7 @@ public partial class IntranetController
[EntityHelper.EntityName(EntityTypes.ServiceRequest)] = [EntityHelper.EntityName(EntityTypes.ServiceRequest)] =
new fds.FdsMfrClient.DatabaseSchema(EntityTypes.ServiceRequest) new fds.FdsMfrClient.DatabaseSchema(EntityTypes.ServiceRequest)
}; };
using var mfr = new fds.FdsMfrClient(); using var mfr = _mfrFactory.Create();
await mfr.Update__entitytable(EntityTypes.ServiceRequest, await mfr.Update__entitytable(EntityTypes.ServiceRequest,
fds.FdsMfr.UpdateNeed.Reset, ids.ToArray(), schemaDic: schemaDic); fds.FdsMfr.UpdateNeed.Reset, ids.ToArray(), schemaDic: schemaDic);
return Ok(); return Ok();
@@ -258,8 +261,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true) if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{ {
string invId = frdic["Id"]?.ToString() ?? ""; string invId = frdic["Id"]?.ToString() ?? "";
var fdInv = new FdsInvoiceData(invId, this); var fdInv = await _invoices.LoadInvoiceAsync(invId, UserAccountID, DbSec);
byte[] filebyte = await fdInv.StoreInvoiceDocumentFile(this); byte[] filebyte = await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec);
var dtset = await getSQLDataSet_async( var dtset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @authuser;", "EXECUTE [dbo].[fds__getInvoice] @Id, @authuser;",
_intranet.Intranet__SQLConnectionString, _intranet.Intranet__SQLConnectionString,
@@ -293,19 +296,19 @@ public partial class IntranetController
private async Task<IActionResult> HandleRequestIdoc(string fn, string id, string code) private async Task<IActionResult> HandleRequestIdoc(string fn, string id, string code)
{ {
if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404); if (!HasForm("id") || string.IsNullOrEmpty(Form("id"))) return StatusCode(404);
var fdInv = new FdsInvoiceData(Form("id"), this); var fdInv = await _invoices.LoadInvoiceAsync(Form("id"), UserAccountID, DbSec);
if (string.IsNullOrEmpty(fdInv.Id)) return StatusCode(404, new { error = "Rechnung wurde nicht gefunden" }); if (string.IsNullOrEmpty(fdInv.Id)) return StatusCode(404, new { error = "Rechnung wurde nicht gefunden" });
string filename = fdInv.InvoiceRegistration.nz("DocumentName").ne($"Rechnung_{fdInv.Id}.pdf"); string filename = fdInv.InvoiceRegistration!.nz("DocumentName").ne($"Rechnung_{fdInv.Id}.pdf");
if (Form("typ") != "img") if (Form("typ") != "img")
{ {
byte[]? ct = Form("create", "0") != "1" byte[]? ct = Form("create", "0") != "1"
? await fdInv.GetInvoiceFile(this) is { Length: > 0 } f1 ? f1 : await fdInv.StoreInvoiceDocumentFile(this) ? await _invoices.GetInvoiceFileAsync(fdInv, fdInv.IsDraft, _mfr) is { Length: > 0 } f1 ? f1 : await _invoices.StoreInvoiceDocumentFileAsync(fdInv, fdInv.IsDraft, UserAccountID, DbSec)
: FuchsPdf.DocToPdfBytes(fdInv.InvoicePDF(this)); : _pdf.DocToPdfBytes(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return ct != null return ct != null
? await FileContentResultAsync(ct, "application/pdf", filename, inline: true) ? await FileContentResultAsync(ct, "application/pdf", filename, inline: true)
: StatusCode(500, new { error = "Rechnungs-PDF konnte nicht erstellt werden" }); : StatusCode(500, new { error = "Rechnungs-PDF konnte nicht erstellt werden" });
} }
var imgcol = await FuchsPdf.DocToImageCollection(fdInv.InvoicePDF(this)); var imgcol = await _pdf.DocToImageCollectionAsync(_invoices.GenerateInvoicePdf(fdInv, fdInv.IsDraft));
return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages }); return await JSONAsync(new { id = fdInv.Id, img = imgcol.ImgB64Array, total = imgcol.TotalPages });
} }
@@ -322,8 +325,8 @@ public partial class IntranetController
if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true) if (frdic.TryGetValue("IsFinal", out var isFinal) && isFinal is true)
{ {
string invId = frdic["Id"]?.ToString() ?? ""; string invId = frdic["Id"]?.ToString() ?? "";
var fdInv = new FdsInvoiceData(invId, this); var fdInv = await _invoices.LoadInvoiceAsync(invId, UserAccountID, DbSec);
byte[] filebyte = FuchsPdf.DocToPdfBytes(fdInv.InvoicePDF(this)); byte[] filebyte = await _invoices.RenderInvoicePdfBytesAsync(fdInv, fdInv.IsDraft);
string email = frdic.nz("SendToEmail", ""); string email = frdic.nz("SendToEmail", "");
if (!string.IsNullOrEmpty(email) && filebyte.Length > 0) if (!string.IsNullOrEmpty(email) && filebyte.Length > 0)
{ {
+41 -6
View File
@@ -26,6 +26,13 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
internal readonly fds.IFdsMfr _mfr; internal readonly fds.IFdsMfr _mfr;
private readonly ILogger<IntranetController> _logger; private readonly ILogger<IntranetController> _logger;
private readonly IComService _comService; private readonly IComService _comService;
private readonly IBankingService _banking;
private readonly IPdfService _pdf;
private readonly IMfrClientFactory _mfrFactory;
private readonly IWidgetService _widgets;
private readonly IReportService _reports;
private readonly IInvoiceService _invoices;
private readonly IReminderService _reminders;
private readonly List<string> _allowedNonAuth = new() { "spwc", "spw" }; private readonly List<string> _allowedNonAuth = new() { "spwc", "spw" };
private readonly List<string> _allowedGet = new() private readonly List<string> _allowedGet = new()
{ {
@@ -41,12 +48,40 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
public string UserAccountID => UserIdent.UserAccountId; public string UserAccountID => UserIdent.UserAccountId;
public string AuthAccount => UserIdent.Email; public string AuthAccount => UserIdent.Email;
public IntranetController(Fuchs_intranet intranet, fds.IFdsMfr mfr, ILogger<IntranetController> logger, IComService comService) public IntranetController(
Fuchs_intranet intranet,
fds.IFdsMfr mfr,
ILogger<IntranetController> logger,
IComService comService,
IBankingService banking,
IPdfService pdf,
IMfrClientFactory mfrFactory,
IWidgetService widgets,
IReportService reports,
IInvoiceService invoices,
IReminderService reminders)
{ {
_intranet = intranet; _intranet = intranet;
_mfr = mfr; _mfr = mfr;
_logger = logger; _logger = logger;
_comService = comService; _comService = comService;
_banking = banking;
_pdf = pdf;
_mfrFactory = mfrFactory;
_widgets = widgets;
_reports = reports;
_invoices = invoices;
_reminders = reminders;
}
/// <summary>Merged query-string + form parameters (form wins) for report processing.</summary>
internal Dictionary<string, string> RequestParamsDict()
{
var prms = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var kv in Request.Query) prms[kv.Key] = kv.Value.ToString();
if (Request.HasFormContentType)
foreach (var kv in Request.Form) prms[kv.Key] = kv.Value.ToString();
return prms;
} }
// ── Standard param list (pre-populates @authuser) ──────────────────────── // ── Standard param list (pre-populates @authuser) ────────────────────────
@@ -106,7 +141,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
IActionResult? result = fn.ToLower() switch IActionResult? result = fn.ToLower() switch
{ {
"ping" => Ok(), "ping" => Ok(),
"wdg" => await FuchsWidgets.IntranetWdg(this, id), "wdg" => await _widgets.GetWidgetAsync(id, UserAccountID, DbSec, Request),
"todos" => new PhysicalFileResult( "todos" => new PhysicalFileResult(
Path.Combine(Directory.GetCurrentDirectory(), "Data", "ProjectToDos.html"), Path.Combine(Directory.GetCurrentDirectory(), "Data", "ProjectToDos.html"),
"text/html"), "text/html"),
@@ -374,7 +409,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
if (string.IsNullOrEmpty(id)) if (string.IsNullOrEmpty(id))
{ {
// Empty id → return the OData EDMX schema ($metadata), matching legacy fds.getSchema() // Empty id → return the OData EDMX schema ($metadata), matching legacy fds.getSchema()
using var mfrSchema = new fds.FdsMfrClient(); using var mfrSchema = _mfrFactory.Create();
string schema = await mfrSchema.ReadAnything( string schema = await mfrSchema.ReadAnything(
mfrSchema.ClientConfig.BaseUrl + "$metadata", throwErrorIfNotOk: false); mfrSchema.ClientConfig.BaseUrl + "$metadata", throwErrorIfNotOk: false);
return Content(schema, "text/xml", System.Text.Encoding.UTF8); return Content(schema, "text/xml", System.Text.Encoding.UTF8);
@@ -383,7 +418,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
{ {
string path = id + (!string.IsNullOrEmpty(code) ? "/" + code : HttpUtility.UrlDecode(Request.QueryString.Value ?? "")); string path = id + (!string.IsNullOrEmpty(code) ? "/" + code : HttpUtility.UrlDecode(Request.QueryString.Value ?? ""));
_logger.LogDebug("HandleMfr reading OData path={Path} user={User}", path, UserAccountID); _logger.LogDebug("HandleMfr reading OData path={Path} user={User}", path, UserAccountID);
using var mfrRead = new fds.FdsMfrClient(); using var mfrRead = _mfrFactory.Create();
var result = await mfrRead.ReadOData(path, throwErrorIfNotOk: false); var result = await mfrRead.ReadOData(path, throwErrorIfNotOk: false);
_logger.LogDebug("HandleMfr OData read complete for path={Path} user={User}", path, UserAccountID); _logger.LogDebug("HandleMfr OData read complete for path={Path} user={User}", path, UserAccountID);
return Content(JsonConvert.SerializeObject(result), "application/json"); return Content(JsonConvert.SerializeObject(result), "application/json");
@@ -402,7 +437,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
if (et != EntityTypes.none && string.IsNullOrEmpty(Request.Form["need"])) if (et != EntityTypes.none && string.IsNullOrEmpty(Request.Form["need"]))
{ {
_logger.LogInformation("MfrUpdate entity={EntityType} need=Short user={User}", et, UserAccountID); _logger.LogInformation("MfrUpdate entity={EntityType} need=Short user={User}", et, UserAccountID);
using var mfrSingle = new fds.FdsMfrClient(); using var mfrSingle = _mfrFactory.Create();
await mfrSingle.Update__entitytable(et, fds.FdsMfr.UpdateNeed.Short); await mfrSingle.Update__entitytable(et, fds.FdsMfr.UpdateNeed.Short);
_logger.LogDebug("MfrUpdate Short completed for entity={EntityType}", et); _logger.LogDebug("MfrUpdate Short completed for entity={EntityType}", et);
return Ok(); return Ok();
@@ -411,7 +446,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller
{ {
var need = fds.FdsMfr.UpdateNeedValue(needParam); var need = fds.FdsMfr.UpdateNeedValue(needParam);
_logger.LogInformation("MfrUpdate entity={EntityType} need={Need} user={User}", et, need, UserAccountID); _logger.LogInformation("MfrUpdate entity={EntityType} need={Need} user={User}", et, need, UserAccountID);
using var mfr = new fds.FdsMfrClient(); using var mfr = _mfrFactory.Create();
await mfr.Update__entitytable(et, updateNeed: need, debugDetails: false); await mfr.Update__entitytable(et, updateNeed: need, debugDetails: false);
_logger.LogDebug("MfrUpdate completed for entity={EntityType} need={Need}", et, need); _logger.LogDebug("MfrUpdate completed for entity={EntityType} need={Need}", et, need);
return Ok(); return Ok();
+4
View File
@@ -7,6 +7,10 @@
<Configurations>db-dev.processweb.de;Debug;Release;server02.processweb.de</Configurations> <Configurations>db-dev.processweb.de;Debug;Release;server02.processweb.de</Configurations>
<NoWarn>CA1416</NoWarn> <NoWarn>CA1416</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<!-- Expose internal members (e.g. FdsInvoiceData.BuildInvoiceParams) to the test project -->
<InternalsVisibleTo Include="Fuchs.Tests" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
</PropertyGroup> </PropertyGroup>
+9
View File
@@ -75,6 +75,15 @@ public class Program
builder.Services.Configure<ProcessWebComSettings>(builder.Configuration.GetSection("Fuchs:Mailer")); builder.Services.Configure<ProcessWebComSettings>(builder.Configuration.GetSection("Fuchs:Mailer"));
builder.Services.AddHttpClient("ProcessWebMailer"); builder.Services.AddHttpClient("ProcessWebMailer");
builder.Services.AddScoped<IComService, ProcessWebComService>(); builder.Services.AddScoped<IComService, ProcessWebComService>();
// Business services (DI migration — replaces the static helper / Active-Record pattern)
builder.Services.AddSingleton<IBankingService, BankingService>(); // stateless parser
builder.Services.AddSingleton<IPdfService, FuchsPdfService>(); // stateless renderer (sets license)
builder.Services.AddSingleton<IMfrClientFactory, MfrClientFactory>();
builder.Services.AddScoped<IWidgetService, FuchsWidgetService>();
builder.Services.AddScoped<IReportService, FuchsReportService>();
builder.Services.AddScoped<IInvoiceService, InvoiceService>();
builder.Services.AddScoped<IReminderService, ReminderService>();
} }
private static void ConfigureApp(WebApplication app) private static void ConfigureApp(WebApplication app)
+117 -8
View File
@@ -1,27 +1,136 @@
using Microsoft.AspNetCore.Mvc; using Fuchs.intranet;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OCORE.security; using OCORE.security;
using OCORE.SQL; using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
namespace Fuchs.Services; namespace Fuchs.Services;
/// <summary> /// <summary>
/// Report service implementation. Replaces the static <c>FuchsReports</c> class. /// Report processing for the Fuchs intranet — SQL-driven reports from the
/// fds__ report catalog, rendered as HTML pages, HTML fragments, or PNG charts
/// via <see cref="FuchsVisualization"/>. Replaces the static <c>FuchsReports</c>.
/// </summary> /// </summary>
public class FuchsReportService : IReportService public class FuchsReportService : IReportService
{ {
private const int DefaultReloadSeconds = 60 * 10;
private readonly Fuchs_intranet _intranet;
private readonly ILogger<FuchsReportService> _logger; private readonly ILogger<FuchsReportService> _logger;
public FuchsReportService(ILogger<FuchsReportService> logger) public FuchsReportService(Fuchs_intranet intranet, ILogger<FuchsReportService> logger)
{ {
_intranet = intranet;
_logger = logger; _logger = logger;
} }
public Task<IActionResult> ProcessRequestAsync(string action, string id, private string Conn => _intranet.Intranet__SQLConnectionString;
string userAccountId, DatabaseSecurity dbSec)
public async Task<IActionResult> ProcessRequestAsync(string fnc, string reportId,
string userAccountId, DatabaseSecurity dbSec, IDictionary<string, string> parameters)
{ {
// Specific report actions are dispatched here. var prms = new Dictionary<string, string>(parameters, StringComparer.OrdinalIgnoreCase)
// Extend with additional cases as needed. {
return Task.FromResult<IActionResult>(new OkResult()); ["@authuser"] = userAccountId
};
string tgt = (string.IsNullOrEmpty(fnc)
? (prms.TryGetValue("fnc", out var f) ? f : fnc)
: fnc).Replace("gct", "generic_content");
string report = prms.TryGetValue("report", out var r) && !string.IsNullOrEmpty(r)
? r
: (!string.IsNullOrEmpty(reportId) ? reportId : "");
string templatePath = Path.Combine(AppContext.BaseDirectory, "Content", "FDS_Template.html");
// Report configuration (refresh interval + cache flag) from the catalog.
int ciRefresh = -2;
bool ciCache = false;
try
{
var catalog = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__admin_getReportCatalog] @report_name, @authuser;",
Conn,
new List<SqlParameter> { SQL_VarChar("@report_name", report), SQL_VarChar("@authuser", userAccountId) },
Security: dbSec, options: new FIS_SQLOptions());
var cfg = catalog.FirstRow.toObjectDictionary();
if (cfg.TryGetValue("refresh", out var rf) && rf is not null && rf is not DBNull &&
int.TryParse(rf.ToString(), out var rfi)) ciRefresh = rfi;
if (cfg.TryGetValue("functions", out var fn) && fn is not null)
ciCache = (fn.ToString() ?? "").Split(',').Contains("cache");
}
catch (Exception cex)
{
_logger.LogError(cex, "Report catalog read failed for report {Report}", report);
}
bool ciForce = prms.TryGetValue("cache", out var ca) && ca.ToLower() == "0";
try
{
switch (tgt)
{
case "generic_content":
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
return new ContentResult
{
Content = await FuchsVisualization.RenderContentAsync(
Conn, dbSec, userAccountId, report, FdsQueryType.generic, prms),
ContentType = "text/html"
};
case "generic":
{
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
var page = await FuchsVisualization.RenderPageAsync(
Conn, dbSec, userAccountId, report, report, FdsQueryType.generic, prms,
FdsDestination.web, templatePath, allowcache: ciCache, forceReload: ciForce);
ApplyReload(page, prms, ciRefresh);
return new ContentResult { Content = page.ToHtml(FdsDestination.web), ContentType = "text/html" };
}
case "chart":
byte[]? png = await FuchsVisualization.RenderQueryAsChartAsync(
Conn, dbSec, userAccountId, report, FdsQueryType.generic, prms);
if (png is null) return new StatusCodeResult(500);
return new FileContentResult(png, "image/png")
{
FileDownloadName = $"{report.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd_HHmm}.png"
};
case "xls":
return new StatusCodeResult(501); // not implemented (matches legacy NotImplementedException)
default:
if (Enum.TryParse<FdsQueryType>(fnc, ignoreCase: true, out var qt))
{
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
var page = await FuchsVisualization.RenderPageAsync(
Conn, dbSec, userAccountId, report, report, qt, prms,
FdsDestination.web, templatePath);
ApplyReload(page, prms, -2);
return new ContentResult { Content = page.ToHtml(FdsDestination.web), ContentType = "text/html" };
}
return new StatusCodeResult(300);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Report processing failed fnc={Fnc} report={Report} tgt={Tgt}", fnc, report, tgt);
return new StatusCodeResult(500);
}
}
private static void ApplyReload(FuchsHtmlPage page, IDictionary<string, string> prms, int ciRefresh)
{
if (prms.TryGetValue("reload", out var rl) && int.TryParse(rl, out var rs)) page.ReloadSeconds = rs;
else if (ciRefresh > -2) page.ReloadSeconds = ciRefresh;
else if (DefaultReloadSeconds > 0) page.ReloadSeconds = DefaultReloadSeconds;
if (page.QueryDuration > 180 && page.ReloadSeconds is > 0 and < 3600) page.ReloadSeconds = 1200;
else if (page.QueryDuration > 60 && page.ReloadSeconds is > 0 and < 1200) page.ReloadSeconds = 1200;
} }
} }
+109 -37
View File
@@ -1,7 +1,6 @@
using Fuchs.intranet; using Fuchs.intranet;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OCORE.security; using OCORE.security;
using OCORE.SQL; using OCORE.SQL;
@@ -12,8 +11,9 @@ using static OCORE.web.mvc_helper_async;
namespace Fuchs.Services; namespace Fuchs.Services;
/// <summary> /// <summary>
/// Widget service implementation. Replaces the static <c>FuchsWidgets</c> class. /// Widget service for the Fuchs intranet dashboard. Port of fuchs_fds_widgets.vb —
/// No longer depends on <c>IntranetController</c>. /// SQL-driven widget cases. Replaces the static <c>FuchsWidgets</c> class
/// (no longer depends on <c>IntranetController</c>).
/// </summary> /// </summary>
public class FuchsWidgetService : IWidgetService public class FuchsWidgetService : IWidgetService
{ {
@@ -26,6 +26,8 @@ public class FuchsWidgetService : IWidgetService
_logger = logger; _logger = logger;
} }
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<IActionResult> GetWidgetAsync(string widgetId, string userAccountId, public async Task<IActionResult> GetWidgetAsync(string widgetId, string userAccountId,
DatabaseSecurity dbSec, HttpRequest request) DatabaseSecurity dbSec, HttpRequest request)
{ {
@@ -33,9 +35,9 @@ public class FuchsWidgetService : IWidgetService
{ {
return widgetId.ToLower() switch return widgetId.ToLower() switch
{ {
"my" => await HandleWidgetMy(userAccountId, dbSec), "my" => await HandleWidgetMy(userAccountId, dbSec),
"one" => await HandleWidgetOne(userAccountId, dbSec, request), "one" => await HandleWidgetOne(userAccountId, dbSec, request),
_ => await HandleWidgetGeneric(widgetId, userAccountId, dbSec) _ => await HandleWidgetGeneric(widgetId, userAccountId, dbSec)
}; };
} }
catch (Exception ex) catch (Exception ex)
@@ -45,20 +47,20 @@ public class FuchsWidgetService : IWidgetService
} }
} }
private List<SqlParameter> MakeParams(string userAccountId, params SqlParameter[] extra) private static List<Microsoft.Data.SqlClient.SqlParameter> Params(string userAccountId,
params Microsoft.Data.SqlClient.SqlParameter[] extra)
{ {
var list = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId) }; var list = new List<Microsoft.Data.SqlClient.SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
list.AddRange(extra); list.AddRange(extra);
return list; return list;
} }
// ── "my" — list of widget short-names for the current user ───────────────
private async Task<IActionResult> HandleWidgetMy(string userAccountId, DatabaseSecurity dbSec) private async Task<IActionResult> HandleWidgetMy(string userAccountId, DatabaseSecurity dbSec)
{ {
var dt = await getSQLDatatable_async( var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser);", "SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser);",
_intranet.Intranet__SQLConnectionString, Conn, Params(userAccountId, SQL_VarChar("@account", "fis")), Security: dbSec);
MakeParams(userAccountId, SQL_VarChar("@account", "fis")),
Security: dbSec);
var names = dt.DataTable.Rows var names = dt.DataTable.Rows
.Cast<System.Data.DataRow>() .Cast<System.Data.DataRow>()
.OrderBy(r => dt.DataTable.Columns.Contains("order") ? r.nz("order") : "") .OrderBy(r => dt.DataTable.Columns.Contains("order") ? r.nz("order") : "")
@@ -67,6 +69,7 @@ public class FuchsWidgetService : IWidgetService
return await JSONAsync(names); return await JSONAsync(names);
} }
// ── "one" — full widget data for a single widget ──────────────────────────
private async Task<IActionResult> HandleWidgetOne(string userAccountId, DatabaseSecurity dbSec, private async Task<IActionResult> HandleWidgetOne(string userAccountId, DatabaseSecurity dbSec,
HttpRequest request) HttpRequest request)
{ {
@@ -75,55 +78,124 @@ public class FuchsWidgetService : IWidgetService
var dt = await getSQLDatatable_async( var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser) WHERE [short_name] = @shortname;", "SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser) WHERE [short_name] = @shortname;",
_intranet.Intranet__SQLConnectionString, Conn,
MakeParams(userAccountId, Params(userAccountId,
SQL_VarChar("@shortname", shortName), SQL_VarChar("@shortname", shortName),
SQL_VarChar("@account", "fis")), SQL_VarChar("@account", "fis")),
Security: dbSec); Security: dbSec);
if (dt.Count != 1) return new StatusCodeResult(404); if (dt.Count != 1) return new StatusCodeResult(404);
var wdg = dt.FirstRow.toObjectDictionary(); var wdg = dt.FirstRow.toObjectDictionary();
return await BuildWidgetResponse(userAccountId, dbSec, wdg); return await BuildWidgetResponse(userAccountId, dbSec, shortName, wdg);
} }
// ── Generic widget by id ──────────────────────────────────────────────────
private async Task<IActionResult> HandleWidgetGeneric(string widgetId, string userAccountId, private async Task<IActionResult> HandleWidgetGeneric(string widgetId, string userAccountId,
DatabaseSecurity dbSec) DatabaseSecurity dbSec)
{ {
var pl = MakeParams(userAccountId, SQL_VarChar("@widget", widgetId, dbNull_IfEmpty: true)); var pl = Params(userAccountId, SQL_VarChar("@widget", widgetId, dbNull_IfEmpty: true));
var dset = await getSQLDataSet_async( var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getWidget] @widget, @authuser;", "EXECUTE [dbo].[fds__getWidget] @widget, @authuser;",
_intranet.Intranet__SQLConnectionString, pl, Conn, pl, tablenames: new[] { "admin", "data" }, Security: dbSec);
tablenames: new[] { "admin", "data" },
Security: dbSec);
return await JSONAsync(new return await JSONAsync(new
{ {
admin = dset.Table("admin").FirstRow.toObjectDictionary(), admin = dset.Table("admin").FirstRow.toObjectDictionary(),
data = dset.Tables("data").toArrayofObjectDictionaries() data = dset.Tables("data").toArrayofObjectDictionaries()
}); });
} }
private async Task<IActionResult> BuildWidgetResponse(string userAccountId, // ── Widget renderer dispatcher ────────────────────────────────────────────
DatabaseSecurity dbSec, Dictionary<string, object?> wdg) private async Task<IActionResult> BuildWidgetResponse(string userAccountId, DatabaseSecurity dbSec,
string shortName, Dictionary<string, object?> wdg)
{ {
string type = (wdg.nz("type", "") ?? "").ToLower(); string dbType = (wdg.nz("type", "") ?? "").ToLower();
string sql = wdg.nz("sql", "") ?? ""; string sql = wdg.nz("sql", "") ?? "";
var ropts = ParseRenderingOptions(wdg.nz("rendering_options", "") ?? "");
string name = wdg.nz("name", "") ?? "";
string descr = wdg.nz("description", "") ?? "";
if (type.StartsWith("sql") && !string.IsNullOrEmpty(sql)) object widgetData;
switch (dbType)
{ {
var dt = await getSQLDatatable_async(sql, case "sql_table":
_intranet.Intranet__SQLConnectionString,
MakeParams(userAccountId), Security: dbSec);
return await JSONAsync(new
{ {
name = wdg.nz("name", ""), var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec);
type, widgetData = new
rows = dt.DataTable.Rows {
.Cast<System.Data.DataRow>() name,
.Select(r => r.toObjectDictionary()) description = descr,
.ToArray() type = "table",
}); rendering_options = ropts,
columns = dt.DataTable.Columns.Cast<System.Data.DataColumn>()
.Select(c => c.ColumnName).ToArray(),
data = dt.DataTable.Rows.Cast<System.Data.DataRow>()
.Select(r => r.toObjectDictionary()).ToArray()
};
break;
}
case "sql_indicator":
{
var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec);
var firstRow = dt.DataTable.Rows.Count > 0
? dt.DataTable.Rows[0].toObjectDictionary()
: new Dictionary<string, object?>();
widgetData = new
{
name,
description = descr,
type = "ind",
rendering_options = ropts,
data = new
{
status = firstRow.nz("status", "") ?? "",
value = firstRow.nz("value", "") ?? "",
label = firstRow.nz("label", "") ?? ""
}
};
break;
}
case "html":
widgetData = new
{
name,
description = descr,
type = "html",
rendering_options = ropts,
html = wdg.nz("html", "") ?? ""
};
break;
default:
widgetData = new
{
name,
description = descr,
type = dbType,
rendering_options = ropts,
html = wdg.nz("html", "") ?? "",
url = wdg.nz("url", "") ?? "",
image = wdg.nz("image", "") ?? "",
data = (object)new
{
status = wdg.nz("status", "") ?? "",
value = wdg.nz("value", "") ?? "",
label = wdg.nz("label", "") ?? ""
}
};
break;
} }
return await JSONAsync(wdg); // Wrap under short_name key so the front-end can do response[wi]
return await JSONAsync(new Dictionary<string, object> { [shortName] = widgetData });
} }
private static string[] ParseRenderingOptions(string raw) =>
string.IsNullOrWhiteSpace(raw)
? Array.Empty<string>()
: raw.Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToArray();
} }
-18
View File
@@ -1,18 +0,0 @@
using Microsoft.Data.SqlClient;
namespace Fuchs.Services;
/// <summary>
/// Factory for creating SQL connections to the Fuchs database.
/// </summary>
public interface IDbConnectionFactory
{
/// <summary>Gets the primary FDS connection string.</summary>
string ConnectionString { get; }
/// <summary>Creates and opens a new SQL connection.</summary>
SqlConnection CreateConnection();
/// <summary>Creates a new SQL connection (not opened).</summary>
SqlConnection CreateClosedConnection();
}
+2 -2
View File
@@ -13,8 +13,8 @@ public interface IInvoiceService
/// <summary>Loads an existing invoice by ID.</summary> /// <summary>Loads an existing invoice by ID.</summary>
Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec); Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec);
/// <summary>Registers (creates or updates) an invoice from form data.</summary> /// <summary>Registers (creates or updates) an invoice from a parsed data object.</summary>
Task<FdsInvoiceData> RegisterInvoiceAsync(object formData, bool change, string invId, Task<FdsInvoiceData> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId,
string userAccountId, DatabaseSecurity dbSec); string userAccountId, DatabaseSecurity dbSec);
/// <summary>Generates a PDF document for an invoice.</summary> /// <summary>Generates a PDF document for an invoice.</summary>
+2 -2
View File
@@ -13,8 +13,8 @@ public interface IReminderService
/// <summary>Loads an existing reminder by ID.</summary> /// <summary>Loads an existing reminder by ID.</summary>
Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec); Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec);
/// <summary>Registers (creates) a reminder from form data.</summary> /// <summary>Registers (creates) a reminder from a parsed data object.</summary>
Task<FdsReminderData> RegisterReminderAsync(object formData, bool change, string remId, Task<FdsReminderData> RegisterReminderAsync(FdsReminderData reminder, bool change, string remId,
string userAccountId, DatabaseSecurity dbSec); string userAccountId, DatabaseSecurity dbSec);
/// <summary>Generates a PDF document for a reminder.</summary> /// <summary>Generates a PDF document for a reminder.</summary>
+12 -6
View File
@@ -1,15 +1,21 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using OCORE.security; using OCORE.security;
using OCORE.SQL;
namespace Fuchs.Services; namespace Fuchs.Services;
/// <summary> /// <summary>
/// Abstraction for report processing. /// Abstraction for SQL-driven report processing (HTML page / fragment / PNG chart).
/// </summary> /// </summary>
public interface IReportService public interface IReportService
{ {
/// <summary>Processes a report request.</summary> /// <summary>
Task<IActionResult> ProcessRequestAsync(string action, string id, /// Processes a report request.
string userAccountId, DatabaseSecurity dbSec); /// </summary>
/// <param name="fnc">Target function: "generic", "generic_content"/"gct", "chart", or a query-type name.</param>
/// <param name="reportId">Report name/id (the URL <c>code</c> segment).</param>
/// <param name="userAccountId">Authenticated user id.</param>
/// <param name="dbSec">Database security context.</param>
/// <param name="parameters">Merged query-string + form parameters.</param>
Task<IActionResult> ProcessRequestAsync(string fnc, string reportId,
string userAccountId, DatabaseSecurity dbSec, IDictionary<string, string> parameters);
} }
+98 -20
View File
@@ -1,62 +1,140 @@
using Fuchs.intranet; using System.Data;
using Fuchs.intranet;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel; using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using OCORE.security; using OCORE.security;
using Newtonsoft.Json;
using OCORE.SQL; using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql; using static OCORE.SQL.sql;
namespace Fuchs.Services; namespace Fuchs.Services;
/// <summary> /// <summary>
/// Invoice service implementation. Extracts DB operations from <c>FdsInvoiceData</c> /// Invoice service — load, register, render and store invoices.
/// and PDF generation into a proper DI service. /// Replaces the controller-coupled, sync-over-async logic that previously lived
/// inside <see cref="FdsInvoiceData"/>.
/// </summary> /// </summary>
public class InvoiceService : IInvoiceService public class InvoiceService : IInvoiceService
{ {
private readonly Fuchs_intranet _intranet; private readonly Fuchs_intranet _intranet;
private readonly IPdfService _pdfService; private readonly IPdfService _pdf;
private readonly ILogger<InvoiceService> _logger; private readonly ILogger<InvoiceService> _logger;
public InvoiceService(Fuchs_intranet intranet, IPdfService pdfService, ILogger<InvoiceService> logger) public InvoiceService(Fuchs_intranet intranet, IPdfService pdf, ILogger<InvoiceService> logger)
{ {
_intranet = intranet; _intranet = intranet;
_pdfService = pdfService; _pdf = pdf;
_logger = logger; _logger = logger;
} }
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec) public async Task<FdsInvoiceData> LoadInvoiceAsync(string id, string userAccountId, DatabaseSecurity dbSec)
{ {
// TODO: Complete after FdsInvoiceData is refactored to remove IntranetController dependency var inv = new FdsInvoiceData();
throw new NotImplementedException("InvoiceService.LoadInvoiceAsync pending FdsInvoiceData refactor."); if (string.IsNullOrEmpty(id)) return inv;
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@Id", id),
SQL_Bit("@includefile", false)
};
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @includefile, @authuser;",
Conn, pl, tablenames: new[] { "admin", "inv", "req", "itm" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(dset.Exception))
_logger.LogError("LoadInvoiceAsync sql exception for {Id}: {Ex}", id, dset.Exception);
inv.InvoiceRegistration = new GenericObjectDictionary(dset.Table("inv").FirstRow.toObjectDictionary());
inv.IsDraft = inv.InvoiceRegistration.getItem("IsFinal", false) is not true;
return inv;
} }
public async Task<FdsInvoiceData> RegisterInvoiceAsync(object formData, bool change, string invId, public async Task<FdsInvoiceData> RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId,
string userAccountId, DatabaseSecurity dbSec) string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("InvoiceService.RegisterInvoiceAsync pending FdsInvoiceData refactor."); if (invoice.NewValues == null) return invoice;
var pl = new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId) };
pl.AddRange(invoice.BuildInvoiceParams(change, invId));
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
if (!change)
{
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT;");
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
}
else
{
pl.Add(SQL_VarChar("@InvId", invId));
sqlParts.Add("EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT;");
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
}
if (invoice.RawProvisionLocation.Length > 0)
{
pl.Add(SQL_NVarChar("@ProvisionLocation",
string.Join("\n", invoice.RawProvisionLocation).LeftToFirst("<!--", emptyIfNotFound: false)));
sqlParts.Add("UPDATE [dbo].[fds__invoices] SET [ProvisionLocation] = LEFT(@ProvisionLocation,1000) WHERE [Id] = @Id AND [isFinal] = 0;");
}
var invdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
Conn, pl, tablenames: new[] { "inv", "det", "req", "itm" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(invdset.Exception))
_logger.LogError("RegisterInvoiceAsync sql exception: {Ex}", invdset.Exception);
invoice.InvoiceRegistration = new GenericObjectDictionary(invdset.Table("inv").FirstRow.toObjectDictionary());
return invoice;
} }
public Document GenerateInvoicePdf(FdsInvoiceData invoice, bool draft) public Document GenerateInvoicePdf(FdsInvoiceData invoice, bool draft)
{ {
throw new NotImplementedException("InvoiceService.GenerateInvoicePdf pending FdsInvoiceData refactor."); var reg = invoice.InvoiceRegistration;
var tb = new FuchsPdf.FdsTextBlocks
{
AdminRef = reg?.getString("Id") ?? "",
Address = reg?.getString("SendToAddress") is { Length: > 0 } sa
? sa.Replace("<br>", "\n").Replace("<br/>", "\n").Split('\n').Select(t => t.Trim()).ToArray()
: Array.Empty<string>(),
AdminUser = reg?.getString("UserNameFinalized") ?? "",
AdminUserEmail = reg?.getString("UserEmailFinalized") ?? "",
AdminDatumValue = reg?.getString("DateCreated") is { Length: > 0 } dc ? DateTime.Parse(dc) : DateTime.Now
};
// WriteLetter is effectively synchronous; no Task.Run thread-hop (safe: no sync context in ASP.NET Core).
var doc = _pdf.WriteLetterAsync(tb, draft).GetAwaiter().GetResult();
doc.Info.Title = reg?.getString("InvoiceTitle") ?? "";
_pdf.ApplyInvoice(doc, tb, invoice, draft);
return doc;
} }
public async Task<byte[]> RenderInvoicePdfBytesAsync(FdsInvoiceData invoice, bool draft) public Task<byte[]> RenderInvoicePdfBytesAsync(FdsInvoiceData invoice, bool draft)
{ => Task.FromResult(_pdf.DocToPdfBytes(GenerateInvoicePdf(invoice, draft)));
throw new NotImplementedException("InvoiceService.RenderInvoicePdfBytesAsync pending FdsInvoiceData refactor.");
}
public async Task<byte[]> StoreInvoiceDocumentFileAsync(FdsInvoiceData invoice, bool draft, public async Task<byte[]> StoreInvoiceDocumentFileAsync(FdsInvoiceData invoice, bool draft,
string userAccountId, DatabaseSecurity dbSec) string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("InvoiceService.StoreInvoiceDocumentFileAsync pending FdsInvoiceData refactor."); byte[] ba;
try { ba = await RenderInvoicePdfBytesAsync(invoice, draft); }
catch (Exception ex) { _logger.LogError(ex, "StoreInvoiceDocumentFileAsync render failed for {Id}", invoice.Id); ba = Array.Empty<byte>(); }
if (ba.Length == 0) return Array.Empty<byte>();
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@Id", invoice.Id),
new("@file", SqlDbType.VarBinary) { Value = ba }
};
bool r = await setSQLValue_async(
"EXECUTE [dbo].[fds__setInvoiceFile] @Id, @file;",
Conn, pl, Security: dbSec, options: new FIS_SQLOptions());
return r ? ba : Array.Empty<byte>();
} }
public async Task<byte[]?> GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft, public async Task<byte[]?> GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft, fds.IFdsMfr mfr)
fds.IFdsMfr mfr)
{ {
if (invoice.InvoiceRegistration?.getItem("IsFinal", false) is true) if (invoice.InvoiceRegistration?.getItem("IsFinal", false) is true)
{ {
+119 -17
View File
@@ -1,67 +1,169 @@
using Fuchs.intranet; using System.Data;
using Fuchs.intranet;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MigraDoc.DocumentObjectModel; using MigraDoc.DocumentObjectModel;
using OCORE.security; using OCORE.security;
using OCORE.SQL; using OCORE.SQL;
using static OCORE.commons; using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql; using static OCORE.SQL.sql;
namespace Fuchs.Services; namespace Fuchs.Services;
/// <summary> /// <summary>
/// Reminder service implementation. Extracts DB operations from <c>FdsReminderData</c> /// Reminder service — load, register, render and store reminders.
/// into a proper DI service. /// Replaces the controller-coupled, sync-over-async logic that previously lived
/// inside <see cref="FdsReminderData"/>.
/// </summary> /// </summary>
public class ReminderService : IReminderService public class ReminderService : IReminderService
{ {
private readonly Fuchs_intranet _intranet; private readonly Fuchs_intranet _intranet;
private readonly IPdfService _pdfService; private readonly IPdfService _pdf;
private readonly ILogger<ReminderService> _logger; private readonly ILogger<ReminderService> _logger;
public ReminderService(Fuchs_intranet intranet, IPdfService pdfService, ILogger<ReminderService> logger) public ReminderService(Fuchs_intranet intranet, IPdfService pdf, ILogger<ReminderService> logger)
{ {
_intranet = intranet; _intranet = intranet;
_pdfService = pdfService; _pdf = pdf;
_logger = logger; _logger = logger;
} }
private string Conn => _intranet.Intranet__SQLConnectionString;
public async Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec) public async Task<FdsReminderData> LoadReminderAsync(string id, string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("ReminderService.LoadReminderAsync pending FdsReminderData refactor."); var rem = new FdsReminderData();
if (string.IsNullOrEmpty(id)) return rem;
var pl = new List<SqlParameter>
{
SQL_VarChar("Id", id),
SQL_VarChar("@authuser", userAccountId),
SQL_Bit("@includefile", false)
};
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getReminder] @Id, @includefile, @authuser;",
Conn, pl, tablenames: new[] { "admin", "rem" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(dset.Exception))
_logger.LogError("LoadReminderAsync sql exception for {Id}: {Ex}", id, dset.Exception);
rem.ReminderRegistration = new GenericObjectDictionary(dset.Table("rem").FirstRow.toObjectDictionary());
rem.IsDraft = rem.ReminderRegistration.getItem("IsFinal") is not true;
return rem;
} }
public async Task<FdsReminderData> RegisterReminderAsync(object formData, bool change, string remId, public async Task<FdsReminderData> RegisterReminderAsync(FdsReminderData reminder, bool change, string remId,
string userAccountId, DatabaseSecurity dbSec) string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("ReminderService.RegisterReminderAsync pending FdsReminderData refactor."); if (reminder.Rem == null || reminder.Rem.Count == 0) return reminder;
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("InvId", reminder.RawInvId),
SQL_Char("type", reminder.Rem["type"]?.ToString() ?? ""),
SQL_Float("amount", stringvalue: reminder.NewValues?.nz("amount") ?? ""),
SQL_Float("amount_payed", stringvalue: reminder.NewValues?.nz("amount_payed") ?? ""),
SQL_VarChar("SendToAddress", string.Join("\n", reminder.RawInvoiceAddress)),
SQL_NVarChar("SendToEmail", reminder.RawInvoiceEmail),
SQL_NVarChar("subject", reminder.NewValues?.getString("subject") ?? "", dbNull_IfEmpty: true),
SQL_NVarChar("text", reminder.NewValues?.nz("text") ?? "", dbNull_IfEmpty: true)
};
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
if (!change || string.IsNullOrEmpty(remId))
sqlParts.Add("EXECUTE [dbo].[fds__createReminder] @InvId, @type, @amount, @amount_payed, @SendToAddress, @SendToEmail, @subject, @text, @authuser, @Id OUTPUT;");
else
pl.Add(SQL_VarChar("RemId", remId));
var remdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
Conn, pl, tablenames: new[] { "rem" },
Security: dbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(remdset.Exception))
_logger.LogError("RegisterReminderAsync sql exception: {Ex}", remdset.Exception);
reminder.ReminderRegistration = new GenericObjectDictionary(remdset.Table("rem").FirstRow.toObjectDictionary());
return reminder;
} }
public Document GenerateReminderPdf(FdsReminderData reminder, bool draft) public Document GenerateReminderPdf(FdsReminderData reminder, bool draft)
{ {
throw new NotImplementedException("ReminderService.GenerateReminderPdf pending FdsReminderData refactor."); var tb = new FuchsPdf.FdsTextBlocks
{
AdminRef = "",
Address = reminder.InvoiceAddress,
AdminUser = reminder.UserNameFinalized,
AdminUserEmail = reminder.UserEmailFinalized,
AdminDatumValue = reminder.DateCreated ?? DateTime.Now
};
if (!string.IsNullOrEmpty(reminder.RawCustomValues))
{
var o = new GenericObjectDictionary(reminder.RawCustomValues);
string oEmail = (string?)o.getItem("contactEmail") ?? "";
string oName = (string?)o.getItem("contactName") ?? "";
if (!string.IsNullOrEmpty(oEmail) || !string.IsNullOrEmpty(oName))
{
tb.AdminUser = oName;
tb.AdminUserEmail = oEmail;
}
}
var doc = _pdf.WriteLetterAsync(tb, draft).GetAwaiter().GetResult();
doc.Info.Title = reminder.ReminderTitle;
_pdf.ApplyReminder(doc, tb, reminder, draft);
return doc;
} }
public async Task<byte[]> RenderReminderPdfBytesAsync(FdsReminderData reminder, bool draft) public Task<byte[]> RenderReminderPdfBytesAsync(FdsReminderData reminder, bool draft)
{ => Task.FromResult(_pdf.DocToPdfBytes(GenerateReminderPdf(reminder, draft)));
throw new NotImplementedException("ReminderService.RenderReminderPdfBytesAsync pending FdsReminderData refactor.");
}
public async Task<byte[]> StoreReminderDocumentFileAsync(FdsReminderData reminder, bool draft, public async Task<byte[]> StoreReminderDocumentFileAsync(FdsReminderData reminder, bool draft,
string userAccountId, DatabaseSecurity dbSec) string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("ReminderService.StoreReminderDocumentFileAsync pending FdsReminderData refactor."); byte[] ba;
try { ba = await RenderReminderPdfBytesAsync(reminder, draft); }
catch (Exception ex) { _logger.LogError(ex, "StoreReminderDocumentFileAsync render failed for {Id}", reminder.Id); ba = Array.Empty<byte>(); }
if (ba.Length == 0) return Array.Empty<byte>();
var pl = new List<SqlParameter>
{
SQL_VarChar("Id", reminder.Id),
SQL_VarChar("@authuser", userAccountId),
new("@file", SqlDbType.VarBinary) { Value = ba }
};
bool r = await setSQLValue_async(
"EXECUTE [dbo].[fds__setReminderFile] @Id, @file;",
Conn, pl, Security: dbSec, options: new FIS_SQLOptions());
return r ? ba : Array.Empty<byte>();
} }
public async Task<byte[]> GetReminderFileAsync(FdsReminderData reminder, bool draft, public async Task<byte[]> GetReminderFileAsync(FdsReminderData reminder, bool draft,
fds.IFdsMfr mfr, string userAccountId, DatabaseSecurity dbSec) fds.IFdsMfr mfr, string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("ReminderService.GetReminderFileAsync pending FdsReminderData refactor."); if (reminder.ReminderRegistration?.getItem("IsFinal", false) is true)
{
if (!reminder.ReminderRegistration.ContainsKey("hasFile") ||
reminder.ReminderRegistration.getItem("hasFile", false) is not true)
await StoreReminderDocumentFileAsync(reminder, draft, userAccountId, dbSec);
byte[]? ba = null;
mfr.GetFdsDoc(ref ba, reminder.Id, "reminder");
return ba ?? Array.Empty<byte>();
}
return await RenderReminderPdfBytesAsync(reminder, draft);
} }
public async Task<(FileInfo? file, byte[]? content)> GetStoredFileAsync(string reminderId, public async Task<(FileInfo? file, byte[]? content)> GetStoredFileAsync(string reminderId,
string userAccountId, DatabaseSecurity dbSec) string userAccountId, DatabaseSecurity dbSec)
{ {
throw new NotImplementedException("ReminderService.GetStoredFileAsync pending FdsReminderData refactor."); var dset = await getSQLDataSet_async(
"SELECT TOP(1) * FROM [dbo].[fds__reminder] WHERE [Id] = @Id AND [file] is not null;",
Conn,
new List<SqlParameter> { SQL_VarChar("@authuser", userAccountId), SQL_VarChar("@Id", reminderId) },
Security: dbSec, options: new FIS_SQLOptions());
var row = dset.FirstTable().FirstRow.toObjectDictionary();
if (row.Count > 0 && !string.IsNullOrEmpty(row.nz("DocumentName")) && row.no("file", null!) is byte[] b)
return (new FileInfo(row.nz("DocumentName")), b);
return (null, null);
} }
} }
-125
View File
@@ -1,125 +0,0 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Data;
using programmersdigest.MT940Parser;
namespace Fuchs.intranet;
/// <summary>
/// MT940 bank statement parser helpers.
/// </summary>
public static class Banking
{
public static string DebitCreditMarkAbb(DebitCreditMark mark) => mark switch
{
DebitCreditMark.Credit => "C",
DebitCreditMark.Debit => "D",
DebitCreditMark.ReverseCredit => "RC",
DebitCreditMark.ReverseDebit => "RD",
_ => ""
};
public static DataTable ParseToDatatable(Stream stream, DataTable? schemaDatatable = null,
ILogger? logger = null)
{
logger ??= NullLogger.Instance;
var tbl = schemaDatatable?.Clone() ?? BuildDefaultSchema();
void SetNfo(DataRow nr, string key, object? value)
{
if (tbl.Columns.Contains(key) && value != null)
nr[key] = value;
}
using var ps = new Parser(stream: stream);
try
{
foreach (var statement in ps.Parse())
{
if (string.IsNullOrEmpty(statement.AccountIdentification)) continue;
foreach (var line in statement.Lines)
{
try
{
var nr = tbl.NewRow();
SetNfo(nr, "AccountIdentification", statement.AccountIdentification);
if (line.Amount.HasValue) SetNfo(nr, "Amount", line.Amount);
if (line.EntryDate.HasValue) SetNfo(nr, "EntryDate", line.EntryDate);
if (line.FundsCode.HasValue) SetNfo(nr, "FundsCode", line.FundsCode.ToString());
SetNfo(nr, "BankReference", line.BankReference);
var info = line.InformationToOwner;
SetNfo(nr, "AccountNumberOfPayer", info.AccountNumberOfPayer);
SetNfo(nr, "BankCodeOfPayer", info.BankCodeOfPayer);
SetNfo(nr, "CompensationAmount", info.CompensationAmount);
SetNfo(nr, "CreditorReference", info.CreditorReference);
SetNfo(nr, "CreditorsReferenceParty", info.CreditorsReferenceParty);
SetNfo(nr, "CustomerReference", info.CustomerReference);
SetNfo(nr, "EndToEndReference", info.EndToEndReference);
SetNfo(nr, "JournalNumber", info.JournalNumber);
SetNfo(nr, "MandateReference", info.MandateReference);
SetNfo(nr, "NameOfPayer", info.NameOfPayer);
SetNfo(nr, "OriginalAmount", info.OriginalAmount);
SetNfo(nr, "OriginatorsIdentificationCode", info.OriginatorsIdentificationCode);
SetNfo(nr, "PayersReferenceParty", info.PayersReferenceParty);
SetNfo(nr, "PostingText", info.PostingText);
SetNfo(nr, "SepaRemittanceInformation", info.SepaRemittanceInformation);
if (info.TextKeyAddition.HasValue) SetNfo(nr, "TextKeyAddition", info.TextKeyAddition);
SetNfo(nr, "TransactionCode", info.TransactionCode);
SetNfo(nr, "IsUnstructuredData", info.IsUnstructuredData);
SetNfo(nr, "UnstructuredData", info.UnstructuredData);
SetNfo(nr, "UnstructuredRemittanceInformation",info.UnstructuredRemittanceInformation);
SetNfo(nr, "DebitCreditMark", DebitCreditMarkAbb(line.Mark));
SetNfo(nr, "SupplementaryDetails",line.SupplementaryDetails);
SetNfo(nr, "TransactionTypeIdCode",line.TransactionTypeIdCode);
SetNfo(nr, "ValueDate", line.ValueDate);
tbl.Rows.Add(nr);
}
catch (Exception ex) { logger.LogWarning(ex, "MT940 line parse error — account={Account}", statement.AccountIdentification); }
}
}
}
catch (Exception ex) { logger.LogError(ex, "MT940 statement parse failed."); }
tbl.AcceptChanges();
return tbl;
}
private static DataTable BuildDefaultSchema()
{
var t = new DataTable();
var cols = t.Columns;
cols.Add("AccountIdentification", typeof(string));
cols.Add("Amount", typeof(decimal));
cols.Add("BankReference", typeof(string));
cols.Add("EntryDate", typeof(DateTime));
cols.Add("FundsCode", typeof(string));
cols.Add("AccountNumberOfPayer", typeof(string));
cols.Add("BankCodeOfPayer", typeof(string));
cols.Add("CompensationAmount", typeof(string));
cols.Add("CreditorReference", typeof(string));
cols.Add("CreditorsReferenceParty", typeof(string));
cols.Add("CustomerReference", typeof(string));
cols.Add("EndToEndReference", typeof(string));
cols.Add("JournalNumber", typeof(string));
cols.Add("MandateReference", typeof(string));
cols.Add("NameOfPayer", typeof(string));
cols.Add("OriginalAmount", typeof(string));
cols.Add("OriginatorsIdentificationCode", typeof(string));
cols.Add("PayersReferenceParty", typeof(string));
cols.Add("PostingText", typeof(string));
cols.Add("SepaRemittanceInformation", typeof(string));
cols.Add("TextKeyAddition", typeof(int));
cols.Add("TransactionCode", typeof(int));
cols.Add("IsUnstructuredData", typeof(bool));
cols.Add("UnstructuredData", typeof(string));
cols.Add("UnstructuredRemittanceInformation", typeof(string));
cols.Add("DebitCreditMark", typeof(string));
cols.Add("SupplementaryDetails", typeof(string));
cols.Add("TransactionTypeIdCode", typeof(string));
cols.Add("ValueDate", typeof(DateTime));
return t;
}
}
+23 -157
View File
@@ -1,10 +1,5 @@
using System.Data;
using Fuchs.Controllers;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OCORE.SQL;
using static OCORE.OCORE_dictionaries; using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql; using static OCORE.SQL.sql;
using static OCORE.commons; using static OCORE.commons;
@@ -12,25 +7,26 @@ using static OCORE.commons;
namespace Fuchs.intranet; namespace Fuchs.intranet;
/// <summary> /// <summary>
/// Encapsulates invoice (Rechnung) data. Converted from VB fds__invoice_data class. /// Invoice (Rechnung) data holder. Converted from VB fds__invoice_data.
/// Pure data + parameter mapping — all persistence and PDF generation now live
/// in <see cref="Fuchs.Services.IInvoiceService"/> (no controller coupling).
/// </summary> /// </summary>
public class FdsInvoiceData public class FdsInvoiceData
{ {
private readonly JObject? _base; private readonly JObject? _base;
private Document? _letter;
public GenericObjectDictionary? Admin { get; private set; } public GenericObjectDictionary? Admin { get; private set; }
public GenericObjectDictionary? NewValues { get; private set; } public GenericObjectDictionary? NewValues { get; private set; }
public GenericObjectDictionary? Sms { get; private set; } public GenericObjectDictionary? Sms { get; private set; }
public List<Dictionary<string, object>>? Req { get; private set; } public List<Dictionary<string, object>>? Req { get; private set; }
public GenericObjectDictionary? InvoiceRegistration { get; private set; } public GenericObjectDictionary? InvoiceRegistration { get; internal set; }
public bool IsDraft { get; private set; } = true; public bool IsDraft { get; internal set; } = true;
public string Id => InvoiceRegistration?.getString("Id") ?? ""; public string Id => InvoiceRegistration?.getString("Id") ?? "";
public string PaymentTerms => InvoiceRegistration?.getString("PaymentTerm") ?? ""; public string PaymentTerms => InvoiceRegistration?.getString("PaymentTerm") ?? "";
// -- PDF-facing properties (used by FuchsPdf.ApplyInvoice) ---------------- // -- PDF-facing properties (used by IPdfService.ApplyInvoice) -------------
public string InvoiceType => public string InvoiceType =>
InvoiceRegistration?.getString("InvoiceType").Substr(0, 1) ?? "R"; InvoiceRegistration?.getString("InvoiceType").Substr(0, 1) ?? "R";
public string InvoiceId => public string InvoiceId =>
@@ -56,7 +52,7 @@ public class FdsInvoiceData
{ {
IEnumerable<Dictionary<string, object?>>? itms = IEnumerable<Dictionary<string, object?>>? itms =
itmsObj as IEnumerable<Dictionary<string, object?>> itmsObj as IEnumerable<Dictionary<string, object?>>
?? (itmsObj is Newtonsoft.Json.Linq.JArray ja ?? (itmsObj is JArray ja
? ja.ToObject<List<Dictionary<string, object?>>>() ? ja.ToObject<List<Dictionary<string, object?>>>()
: null); : null);
if (itms != null) result.AddRange(itms); if (itms != null) result.AddRange(itms);
@@ -73,33 +69,33 @@ public class FdsInvoiceData
{ {
var result = new Dictionary<string, Dictionary<string, object?>>(); var result = new Dictionary<string, Dictionary<string, object?>>();
if (InvoiceRegistration == null) return result; if (InvoiceRegistration == null) return result;
// Primary VAT slot
if (FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_1"), out decimal ust1) if (FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_1"), out decimal ust1)
&& FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_net1"), out decimal net1) && FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_net1"), out decimal net1)
&& !(ust1 == 0 && net1 == 0)) && !(ust1 == 0 && net1 == 0))
result[ust1.ToString("0.##")] = new Dictionary<string, object?> result[ust1.ToString("0.##")] = new Dictionary<string, object?> { ["vat_amount"] = net1 };
{ ["vat_amount"] = net1 };
// Secondary VAT slot
if (FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_2"), out decimal ust2) if (FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_2"), out decimal ust2)
&& FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_net2"), out decimal net2) && FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_net2"), out decimal net2)
&& !(ust2 == 0 && net2 == 0)) && !(ust2 == 0 && net2 == 0))
result[ust2.ToString("0.##")] = new Dictionary<string, object?> result[ust2.ToString("0.##")] = new Dictionary<string, object?> { ["vat_amount"] = net2 };
{ ["vat_amount"] = net2 };
return result; return result;
} }
} }
// -- Raw properties -------------------------------------------------------- // -- Raw form properties (used by parameter mapping) -----------------------
private string RawInvoiceAddress => NewValues?.nz("invoiceaddress") ?? ""; internal string RawInvoiceAddress => NewValues?.nz("invoiceaddress") ?? "";
private string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? ""; internal string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? "";
private string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? ""; internal string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? "";
private string RawProvisionPeriod => NewValues?.nz("provisionperiod") ?? ""; internal string RawProvisionPeriod => NewValues?.nz("provisionperiod") ?? "";
private string[] RawProvisionLocation => internal string[] RawProvisionLocation =>
NewValues?.nz("provisionlocation") is { Length: > 0 } s NewValues?.nz("provisionlocation") is { Length: > 0 } s
? s.Replace("\r\n", "\n").Split('\n').Select(t => t.Trim()).Where(t => t != "").ToArray() ? s.Replace("\r\n", "\n").Split('\n').Select(t => t.Trim()).Where(t => t != "").ToArray()
: Array.Empty<string>(); : Array.Empty<string>();
// -- Ctors ----------------------------------------------------------------- // -- Ctors -----------------------------------------------------------------
/// <summary>Empty instance (used when loading from the DB via the service).</summary>
public FdsInvoiceData() { }
/// <summary>Parses form data (admin/new/sms/req) into the data object.</summary>
public FdsInvoiceData(object ctd) public FdsInvoiceData(object ctd)
{ {
_base = ctd as JObject; _base = ctd as JObject;
@@ -113,146 +109,16 @@ public class FdsInvoiceData
} }
} }
public FdsInvoiceData(string id, IntranetController ctrl) => RegisterInvoice(id, ctrl); // -- Parameter mapping (consumed by InvoiceService.RegisterInvoiceAsync) ---
internal List<SqlParameter> BuildInvoiceParams(bool change, string invId)
// -- PDF -------------------------------------------------------------------
public Document InvoicePDF(IntranetController ctrl)
{
if (_letter != null) return _letter;
if (InvoiceRegistration == null) RegisterInvoice(Id, ctrl);
var tb = new FuchsPdf.FdsTextBlocks
{
AdminRef = InvoiceRegistration?.getString("Id") ?? "",
Address = InvoiceRegistration?.getString("SendToAddress") is { Length: > 0 } sa
? sa.Replace("<br>", "\n").Replace("<br/>", "\n").Split('\n').Select(t => t.Trim()).ToArray()
: Array.Empty<string>(),
AdminUser = InvoiceRegistration?.getString("UserNameFinalized") ?? "",
AdminUserEmail = InvoiceRegistration?.getString("UserEmailFinalized") ?? "",
AdminDatumValue = InvoiceRegistration?.getString("DateCreated") is { Length: > 0 } dc ? DateTime.Parse(dc) : DateTime.Now
};
_letter = Task.Run(async () => await FuchsPdf.WriteLetter(tb, draft: IsDraft, locale: FuchsPdf.DeCulture)).Result;
_letter.Info.Title = InvoiceRegistration?.getString("InvoiceTitle") ?? "";
FuchsPdf.ApplyInvoice(_letter, tb, this, draft: IsDraft);
return _letter;
}
// -- File operations -------------------------------------------------------
public async Task<byte[]?> GetInvoiceFile(IntranetController ctrl)
{
if (InvoiceRegistration?.getItem("IsFinal", false) is true)
{
byte[]? ba = null;
ctrl._mfr.GetFdsDoc(ref ba, Id, "invoice");
return ba;
}
return await RenderToPdfBytes(ctrl);
}
public async Task<byte[]> StoreInvoiceDocumentFile(IntranetController ctrl)
{
byte[] ba;
try { ba = await RenderToPdfBytes(ctrl); }
catch { ba = Array.Empty<byte>(); }
if (ba.Length == 0) return Array.Empty<byte>();
var pl = ctrl.StdParamlist(SQL_VarChar("@Id", Id));
pl.Add(new SqlParameter("@file", SqlDbType.VarBinary) { Value = ba });
bool r = await setSQLValue_async(
"EXECUTE [dbo].[fds__setInvoiceFile] @Id, @file;",
ctrl._intranet.Intranet__SQLConnectionString, pl,
Security: ctrl.DbSec, options: new FIS_SQLOptions());
return r ? ba : Array.Empty<byte>();
}
private async Task<byte[]> RenderToPdfBytes(IntranetController ctrl)
{
var pdfrend = new PdfDocumentRenderer() { Document = InvoicePDF(ctrl) };
pdfrend.RenderDocument();
using var ms = new MemoryStream();
pdfrend.PdfDocument.Save(ms, false);
ms.Position = 0;
return OCORE.pdf._pdf.pdfAFileContent(ms.ToArray());
}
// -- Registration ----------------------------------------------------------
public void RegisterInvoice(IntranetController ctrl, bool change, string invId)
{
if (NewValues == null) return;
Task.Run(async () =>
{
try
{
var pl = ctrl.StdParamlist();
pl.AddRange(BuildInvoiceParams(change, invId));
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
if (!change)
{
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice] @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT;");
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
}
else
{
pl.Add(SQL_VarChar("@InvId", invId));
sqlParts.Add("EXECUTE [dbo].[fds__setInvoice] @InvId, @InvoiceType, @InvoiceTitle, @InvoiceBalance, @InvoiceBalance_net, @InvoiceVAT_net1, @InvoiceVAT_1, @PaymentTerm, @CustomerId, @SendToAddress, @SendToEmail, @ProvisionPeriod, @CustomValues, @authuser, @Id OUTPUT;");
sqlParts.Add("EXECUTE [dbo].[fds__createInvoice_Details] @Id, @InvoiceService_net, @InvoiceService_VAT, @InvoiceOptions, @authuser;");
}
if (RawProvisionLocation.Length > 0)
{
pl.Add(SQL_NVarChar("@ProvisionLocation",
string.Join("\n", RawProvisionLocation).LeftToFirst("<!--", emptyIfNotFound: false)));
sqlParts.Add("UPDATE [dbo].[fds__invoices] SET [ProvisionLocation] = LEFT(@ProvisionLocation,1000) WHERE [Id] = @Id AND [isFinal] = 0;");
}
var invdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
ctrl._intranet.Intranet__SQLConnectionString, pl,
tablenames: new[] { "inv", "det", "req", "itm" },
Security: ctrl.DbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(invdset.Exception))
ctrl._intranet.debug_log("FdsInvoiceData.RegisterInvoice - sql exception",
data: new { exception = invdset.Exception });
InvoiceRegistration = new GenericObjectDictionary(invdset.Table("inv").FirstRow.toObjectDictionary());
}
catch (Exception ex) { ctrl._intranet.debug_log("FdsInvoiceData.RegisterInvoice", ex: ex); }
}).Wait();
}
public void RegisterInvoice(string id, IntranetController ctrl)
{
if (string.IsNullOrEmpty(id)) return;
Task.Run(async () =>
{
try
{
var pl = ctrl.StdParamlist(SQL_VarChar("@Id", id));
pl.Add(SQL_Bit("@includefile", false));
var invdset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getInvoice] @Id, @includefile, @authuser;",
ctrl._intranet.Intranet__SQLConnectionString, pl,
tablenames: new[] { "admin", "inv", "req", "itm" },
Security: ctrl.DbSec, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(invdset.Exception))
ctrl._intranet.debug_log("FdsInvoiceData.RegisterInvoice(id) - sql exception",
data: new { exception = invdset.Exception });
InvoiceRegistration = new GenericObjectDictionary(invdset.Table("inv").FirstRow.toObjectDictionary());
IsDraft = InvoiceRegistration.getItem("IsFinal", false) is not true;
}
catch (Exception ex) { ctrl._intranet.debug_log("FdsInvoiceData.RegisterInvoice(id)", ex: ex); }
}).Wait();
}
// -- Param builder ---------------------------------------------------------
private List<SqlParameter> BuildInvoiceParams(bool change, string invId)
{ {
_ = change; _ = invId;
var vatsDic = new Dictionary<string, string>(); var vatsDic = new Dictionary<string, string>();
if (Req != null) if (Req != null)
{ {
foreach (var rq in Req) foreach (var rq in Req)
{ {
if (rq.TryGetValue("items", out var itmsObj) && if (rq.TryGetValue("items", out var itmsObj) && itmsObj is List<object> itms)
itmsObj is List<object> itms)
{ {
foreach (var itm in itms.OfType<Dictionary<string, object?>>()) foreach (var itm in itms.OfType<Dictionary<string, object?>>())
{ {
+14 -167
View File
@@ -1,42 +1,35 @@
using System.Data;
using Fuchs.Controllers;
using Microsoft.Data.SqlClient;
using MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OCORE.SQL;
using static OCORE.OCORE_dictionaries; using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
using static OCORE.commons; using static OCORE.commons;
namespace Fuchs.intranet; namespace Fuchs.intranet;
/// <summary> /// <summary>
/// Encapsulates a reminder (Zahlungserinnerung) data object. /// Reminder (Zahlungserinnerung) data holder. Converted from VB fds__reminder_data.
/// Converted from VB fds__reminder_data class. /// Pure data — persistence and PDF generation live in
/// <see cref="Fuchs.Services.IReminderService"/> (no controller coupling).
/// </summary> /// </summary>
public class FdsReminderData public class FdsReminderData
{ {
private readonly JObject? _base; private readonly JObject? _base;
private Document? _letter;
public GenericObjectDictionary? NewValues { get; private set; } public GenericObjectDictionary? NewValues { get; private set; }
public GenericObjectDictionary? Rem { get; private set; } public GenericObjectDictionary? Rem { get; private set; }
public GenericObjectDictionary? ReminderRegistration { get; private set; } public GenericObjectDictionary? ReminderRegistration { get; internal set; }
public bool IsDraft { get; private set; } = true; public bool IsDraft { get; internal set; } = true;
public string Id => ReminderRegistration?.getString("Id") ?? ""; public string Id => ReminderRegistration?.getString("Id") ?? "";
// -- Raw props from form data --------------------------------------------- // -- Raw props from form data ---------------------------------------------
public string[] RawInvoiceAddress => NewValues?.nz("invoiceaddress") is { Length: > 0 } s internal string[] RawInvoiceAddress => NewValues?.nz("invoiceaddress") is { Length: > 0 } s
? s.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n") ? s.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n")
.Replace("\r\n", "\n").Replace("\n\n", "\n").Split('\n') .Replace("\r\n", "\n").Replace("\n\n", "\n").Split('\n')
.Select(t => System.Web.HttpUtility.HtmlDecode(t.Trim())).Where(t => t != "").ToArray() .Select(t => System.Web.HttpUtility.HtmlDecode(t.Trim())).Where(t => t != "").ToArray()
: Array.Empty<string>(); : Array.Empty<string>();
public string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? ""; internal string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? "";
public string RawInvId => Rem?.nz("invid").ne(Rem?.nz("InvId") ?? "").Trim() ?? ""; internal string RawInvId => Rem?.nz("invid").ne(Rem?.nz("InvId") ?? "").Trim() ?? "";
public string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? ""; internal string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? "";
// -- Computed props from registration ------------------------------------- // -- Computed props from registration -------------------------------------
public DateTime? DateCreated => ReminderRegistration?.getString("DateCreated") is { Length: > 0 } d ? DateTime.Parse(d) : null; public DateTime? DateCreated => ReminderRegistration?.getString("DateCreated") is { Length: > 0 } d ? DateTime.Parse(d) : null;
@@ -59,11 +52,10 @@ public class FdsReminderData
get get
{ {
if (ReminderRegistration == null) return new List<Dictionary<string, object?>>(); if (ReminderRegistration == null) return new List<Dictionary<string, object?>>();
// Items are stored as nested JSON under "invoices" key in the registration
var raw = ReminderRegistration.getItem("invoices"); var raw = ReminderRegistration.getItem("invoices");
try try
{ {
if (raw is Newtonsoft.Json.Linq.JArray ja) if (raw is JArray ja)
return ja.ToObject<List<Dictionary<string, object?>>>() ?? new(); return ja.ToObject<List<Dictionary<string, object?>>>() ?? new();
if (raw is string s && s.StartsWith("[")) if (raw is string s && s.StartsWith("["))
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object?>>>(s) ?? new(); return Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object?>>>(s) ?? new();
@@ -74,6 +66,10 @@ public class FdsReminderData
} }
// -------------------------------- Ctors ---------------------------------- // -------------------------------- Ctors ----------------------------------
/// <summary>Empty instance (used when loading from the DB via the service).</summary>
public FdsReminderData() { }
/// <summary>Parses form data (new/rem) into the data object.</summary>
public FdsReminderData(object ctd) public FdsReminderData(object ctd)
{ {
if (ctd is JObject jo) { _base = jo; } if (ctd is JObject jo) { _base = jo; }
@@ -84,153 +80,4 @@ public class FdsReminderData
if (_base.ContainsKey("rem")) Rem = new GenericObjectDictionary(_base["rem"]!.ToObject<Dictionary<string, object>>()!); if (_base.ContainsKey("rem")) Rem = new GenericObjectDictionary(_base["rem"]!.ToObject<Dictionary<string, object>>()!);
} }
} }
public FdsReminderData(string id, IntranetController ctrl) => RegisterReminder(id, ctrl);
// -------------------------------- PDF ------------------------------------
public Document ReminderPDF(IntranetController ctrl)
{
if (_letter != null) return _letter;
if (ReminderRegistration == null) RegisterReminder(Id, ctrl);
var tb = new FuchsPdf.FdsTextBlocks
{
AdminRef = "",
Address = InvoiceAddress,
AdminUser = UserNameFinalized,
AdminUserEmail = UserEmailFinalized,
AdminDatumValue = DateCreated ?? DateTime.Now
};
if (!string.IsNullOrEmpty(RawCustomValues))
{
var o = new GenericObjectDictionary(RawCustomValues);
string oEmail = (string?)o.getItem("contactEmail") ?? "";
string oName = (string?)o.getItem("contactName") ?? "";
if (!string.IsNullOrEmpty(oEmail) || !string.IsNullOrEmpty(oName))
{
tb.AdminUser = oName;
tb.AdminUserEmail = oEmail;
}
}
_letter = Task.Run(async () => await FuchsPdf.WriteLetter(tb, draft: IsDraft, locale: FuchsPdf.DeCulture)).Result;
_letter.Info.Title = ReminderTitle;
FuchsPdf.ApplyReminder(_letter, tb, this, draft: IsDraft);
return _letter;
}
// --------------------------- File operations -----------------------------
public async Task<byte[]> GetReminderFile(IntranetController ctrl)
{
if (ReminderRegistration?.getItem("IsFinal", false) is true)
{
if (!ReminderRegistration.ContainsKey("hasFile") || ReminderRegistration.getItem("hasFile", false) is not true)
await StoreReminderDocumentFile(ctrl);
byte[]? ba = null;
ctrl._mfr.GetFdsDoc(ref ba, Id, "reminder");
return ba ?? Array.Empty<byte>();
}
return await RenderToPdfBytes(ctrl);
}
public async Task<byte[]> StoreReminderDocumentFile(IntranetController ctrl)
{
byte[] ba;
try { ba = await RenderToPdfBytes(ctrl); }
catch { ba = Array.Empty<byte>(); }
if (ba.Length == 0) return Array.Empty<byte>();
var pl = ctrl.StdParamlist("Id", Id);
pl.Add(new SqlParameter("@file", SqlDbType.VarBinary) { Value = ba });
bool r = await setSQLValue_async("EXECUTE [dbo].[fds__setReminderFile] @Id, @file;",
ctrl._intranet.Intranet__SQLConnectionString, pl,
Security: ctrl.DbSec, options: new FIS_SQLOptions());
return r ? ba : Array.Empty<byte>();
}
private async Task<byte[]> RenderToPdfBytes(IntranetController ctrl)
{
var pdfrend = new PdfDocumentRenderer() { Document = ReminderPDF(ctrl) };
pdfrend.RenderDocument();
using var ms = new MemoryStream();
pdfrend.PdfDocument.Save(ms, false);
ms.Position = 0;
return OCORE.pdf._pdf.pdfAFileContent(ms.ToArray());
}
// ---------------------------- Registration -------------------------------
public void RegisterReminder(IntranetController ctrl, bool change, string remId)
{
if (Rem == null || Rem.Count == 0) return;
Task.Run(async () =>
{
try
{
var pl = ctrl.StdParamlist();
pl.Add(SQL_VarChar("InvId", RawInvId));
pl.Add(SQL_Char("type", Rem["type"]?.ToString() ?? ""));
pl.Add(SQL_Float("amount", stringvalue: NewValues?.nz("amount") ?? ""));
pl.Add(SQL_Float("amount_payed", stringvalue: NewValues?.nz("amount_payed") ?? ""));
pl.Add(SQL_VarChar("SendToAddress", string.Join("\n", RawInvoiceAddress)));
pl.Add(SQL_NVarChar("SendToEmail", RawInvoiceEmail));
pl.Add(SQL_NVarChar("subject", NewValues?.getString("subject") ?? "", dbNull_IfEmpty: true));
pl.Add(SQL_NVarChar("text", NewValues?.nz("text") ?? "", dbNull_IfEmpty: true));
var sqlParts = new List<string> { "DECLARE @Id varchar(10);" };
if (!change || string.IsNullOrEmpty(remId))
sqlParts.Add("EXECUTE [dbo].[fds__createReminder] @InvId, @type, @amount, @amount_payed, @SendToAddress, @SendToEmail, @subject, @text, @authuser, @Id OUTPUT;");
else
pl.Add(SQL_VarChar("RemId", remId));
var remdset = await getSQLDataSet_async(string.Join("\n", sqlParts),
ctrl._intranet.Intranet__SQLConnectionString, pl,
Security: ctrl.DbSec,
tablenames: new[] { "rem" }, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(remdset.Exception))
ctrl._intranet.debug_log("FdsReminderData.RegisterReminder - sql exception",
data: new { exception = remdset.Exception });
ReminderRegistration = new GenericObjectDictionary(remdset.Table("rem").FirstRow.toObjectDictionary());
}
catch (Exception ex) { ctrl._intranet.debug_log("FdsReminderData.RegisterReminder", ex: ex); }
}).Wait();
}
public void RegisterReminder(string id, IntranetController ctrl)
{
if (string.IsNullOrEmpty(id)) return;
Task.Run(async () =>
{
try
{
var pl = ctrl.StdParamlist("Id", id);
pl.Add(SQL_Bit("@includefile", false));
var remdset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getReminder] @Id, @includefile, @authuser;",
ctrl._intranet.Intranet__SQLConnectionString, pl,
Security: ctrl.DbSec,
tablenames: new[] { "admin", "rem" }, options: new FIS_SQLOptions());
if (!string.IsNullOrEmpty(remdset.Exception))
ctrl._intranet.debug_log("FdsReminderData.RegisterReminder(id) - sql exception",
data: new { exception = remdset.Exception });
ReminderRegistration = new GenericObjectDictionary(remdset.Table("rem").FirstRow.toObjectDictionary());
IsDraft = !(ReminderRegistration.getItem("IsFinal") is true);
}
catch (Exception ex) { ctrl._intranet.debug_log("FdsReminderData.RegisterReminder(id)", ex: ex); }
}).Wait();
}
public static FileInfo? GetStoredFile(ref byte[]? file, string reminderId, IntranetController ctrl)
{
var sqlrw = Task.Run(async () =>
(await getSQLDataSet_async(
"SELECT TOP(1) * FROM [dbo].[fds__reminder] WHERE [Id] = @Id AND [file] is not null;",
ctrl._intranet.Intranet__SQLConnectionString,
ctrl.StdParamlist(SQL_VarChar("@Id", reminderId)),
Security: ctrl.DbSec))
.FirstTable().FirstRow.toObjectDictionary()).Result;
if (sqlrw.Count > 0 && !string.IsNullOrEmpty(sqlrw.nz("DocumentName")) && sqlrw.no("file", null!) is byte[] b)
{
file = b;
return new FileInfo(sqlrw.nz("DocumentName"));
}
return null;
}
} }
-124
View File
@@ -1,124 +0,0 @@
using Fuchs.Controllers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
namespace Fuchs.intranet;
/// <summary>
/// Report processing for the Fuchs intranet — SQL-driven reports from the
/// fds__ report catalog, rendered as HTML pages, HTML fragments, or PNG charts.
/// Ported from the legacy fuchs_reports.vb (process_fdsrequest) + ocms_visualization.
/// </summary>
public static class FuchsReports
{
private const int DefaultReloadSeconds = 60 * 10;
/// <param name="ctrl">Current controller (DB, security, request, user).</param>
/// <param name="fnc">Target function (e.g. "generic", "generic_content"/"gct", "chart").</param>
/// <param name="id">Report name/id (the URL <c>code</c> segment).</param>
public static async Task<IActionResult> ProcessFdsRequest(IntranetController ctrl, string fnc, string id)
{
// Merge query string + form into a single parameter map (form wins); force @authuser.
var prms = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var kv in ctrl.Request.Query) prms[kv.Key] = kv.Value.ToString();
if (ctrl.Request.HasFormContentType)
foreach (var kv in ctrl.Request.Form) prms[kv.Key] = kv.Value.ToString();
prms["@authuser"] = ctrl.UserAccountID;
string tgt = (string.IsNullOrEmpty(fnc)
? (prms.TryGetValue("fnc", out var f) ? f : fnc)
: fnc).Replace("gct", "generic_content");
string report = prms.TryGetValue("report", out var r) && !string.IsNullOrEmpty(r)
? r
: (!string.IsNullOrEmpty(id) ? id : "");
string templatePath = Path.Combine(AppContext.BaseDirectory, "Content", "FDS_Template.html");
// Report configuration (refresh interval + cache flag) from the catalog.
int ciRefresh = -2;
bool ciCache = false;
try
{
var catalog = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__admin_getReportCatalog] @report_name, @authuser;",
ctrl._intranet.Intranet__SQLConnectionString,
new List<SqlParameter> { SQL_VarChar("@report_name", report), SQL_VarChar("@authuser", ctrl.UserAccountID) },
Security: ctrl.DbSec, options: new FIS_SQLOptions());
var cfg = catalog.FirstRow.toObjectDictionary();
if (cfg.TryGetValue("refresh", out var rf) && rf is not null && rf is not DBNull &&
int.TryParse(rf.ToString(), out var rfi)) ciRefresh = rfi;
if (cfg.TryGetValue("functions", out var fn) && fn is not null)
ciCache = (fn.ToString() ?? "").Split(',').Contains("cache");
}
catch (Exception cex)
{
ctrl._intranet.debug_log("FuchsReports.ProcessFdsRequest - catalog", ex: cex);
}
bool ciForce = prms.TryGetValue("cache", out var ca) && ca.ToLower() == "0";
try
{
switch (tgt)
{
case "generic_content":
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
string content = await FuchsVisualization.RenderContentAsync(
ctrl, report, FdsQueryType.generic, prms);
return new ContentResult { Content = content, ContentType = "text/html" };
case "generic":
{
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
var page = await FuchsVisualization.RenderPageAsync(
ctrl, report, report, FdsQueryType.generic, prms,
FdsDestination.web, templatePath, allowcache: ciCache, forceReload: ciForce);
ApplyReload(page, prms, ciRefresh);
return new ContentResult { Content = page.ToHtml(FdsDestination.web), ContentType = "text/html" };
}
case "chart":
byte[]? png = await FuchsVisualization.RenderQueryAsChartAsync(
ctrl, report, FdsQueryType.generic, prms);
if (png is null) return new StatusCodeResult(500);
return new FileContentResult(png, "image/png")
{
FileDownloadName = $"{report.Replace(" ", "_")}_{DateTime.Now:yyyyMMdd_HHmm}.png"
};
case "xls":
return new StatusCodeResult(501); // not implemented (matches legacy NotImplementedException)
default:
if (Enum.TryParse<FdsQueryType>(fnc, ignoreCase: true, out var qt))
{
if (string.IsNullOrEmpty(report)) return new StatusCodeResult(300);
var page = await FuchsVisualization.RenderPageAsync(
ctrl, report, report, qt, prms, FdsDestination.web, templatePath);
ApplyReload(page, prms, -2);
return new ContentResult { Content = page.ToHtml(FdsDestination.web), ContentType = "text/html" };
}
return new StatusCodeResult(300);
}
}
catch (Exception ex)
{
ctrl._intranet.debug_log("FuchsReports.ProcessFdsRequest",
ex: ex, data: new { fnc, id, report, tgt });
return new StatusCodeResult(500);
}
}
private static void ApplyReload(FuchsHtmlPage page, IDictionary<string, string> prms, int ciRefresh)
{
if (prms.TryGetValue("reload", out var rl) && int.TryParse(rl, out var rs)) page.ReloadSeconds = rs;
else if (ciRefresh > -2) page.ReloadSeconds = ciRefresh;
else if (DefaultReloadSeconds > 0) page.ReloadSeconds = DefaultReloadSeconds;
if (page.QueryDuration > 180 && page.ReloadSeconds is > 0 and < 3600) page.ReloadSeconds = 1200;
else if (page.QueryDuration > 60 && page.ReloadSeconds is > 0 and < 1200) page.ReloadSeconds = 1200;
}
}
+17 -14
View File
@@ -2,11 +2,11 @@ using System.Data;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Fuchs.Controllers;
using HtmlAgilityPack; using HtmlAgilityPack;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Newtonsoft.Json; using Newtonsoft.Json;
using OCORE.GenericCharts; using OCORE.GenericCharts;
using OCORE.security;
using OCORE.SQL; using OCORE.SQL;
using static OCORE.commons; using static OCORE.commons;
using static OCORE.SQL.sql; using static OCORE.SQL.sql;
@@ -177,11 +177,11 @@ internal sealed class ManagedCache
public static class FuchsVisualization public static class FuchsVisualization
{ {
// ── Query execution ───────────────────────────────────────────────────── // ── Query execution ─────────────────────────────────────────────────────
private static string ConnStr(IntranetController ctrl) => ctrl._intranet.Intranet__SQLConnectionString;
/// <summary>Runs a report (fds__r_ / fds__xls_) and returns its admin table (ADT) + data tables.</summary> /// <summary>Runs a report (fds__r_ / fds__xls_) and returns its admin table (ADT) + data tables.</summary>
public static async Task<(DataTable? adt, List<DataTable> dt)> GetQuery( public static async Task<(DataTable? adt, List<DataTable> dt)> GetQuery(
IntranetController ctrl, string query, IDictionary<string, string> prms) string connStr, DatabaseSecurity dbSec, string userAccountId,
string query, IDictionary<string, string> prms)
{ {
var dtList = new List<DataTable>(); var dtList = new List<DataTable>();
DataTable? adt = null; DataTable? adt = null;
@@ -192,10 +192,10 @@ public static class FuchsVisualization
var reportinfo = await getSQLDataSet_async( var reportinfo = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__admin_getReportCatalog] @report_name, @authuser;", "EXECUTE [dbo].[fds__admin_getReportCatalog] @report_name, @authuser;",
ConnStr(ctrl), connStr,
new List<SqlParameter> { SQL_VarChar("@report_name", query), SQL_VarChar("@authuser", ctrl.UserAccountID) }, new List<SqlParameter> { SQL_VarChar("@report_name", query), SQL_VarChar("@authuser", userAccountId) },
tablenames: new[] { "procedures", "parameter", "categories", "tags" }, tablenames: new[] { "procedures", "parameter", "categories", "tags" },
Security: ctrl.DbSec, options: new FIS_SQLOptions()); Security: dbSec, options: new FIS_SQLOptions());
if (!reportinfo.Contains("procedures") || reportinfo.Tables("procedures").Rows.Count == 0) if (!reportinfo.Contains("procedures") || reportinfo.Tables("procedures").Rows.Count == 0)
return (adt, dtList); return (adt, dtList);
@@ -204,8 +204,8 @@ public static class FuchsVisualization
var procRow = reportinfo.Tables("procedures").Rows[0]; var procRow = reportinfo.Tables("procedures").Rows[0];
string sql = $"EXECUTE [dbo].[{procRow["name"]}] {procRow["parameter"]};"; string sql = $"EXECUTE [dbo].[{procRow["name"]}] {procRow["parameter"]};";
var dset = await getSQLDataSet_async(sql, ConnStr(ctrl), qparams, var dset = await getSQLDataSet_async(sql, connStr, qparams,
Security: ctrl.DbSec, options: new FIS_SQLOptions()); Security: dbSec, options: new FIS_SQLOptions());
if (isXls) if (isXls)
{ {
@@ -456,16 +456,18 @@ public static class FuchsVisualization
/// <summary>Renders a report as a bare HTML fragment (destination = content).</summary> /// <summary>Renders a report as a bare HTML fragment (destination = content).</summary>
public static async Task<string> RenderContentAsync( public static async Task<string> RenderContentAsync(
IntranetController ctrl, string query, FdsQueryType qtype, IDictionary<string, string> prms) string connStr, DatabaseSecurity dbSec, string userAccountId,
string query, FdsQueryType qtype, IDictionary<string, string> prms)
{ {
var (adt, dt) = await GetQuery(ctrl, query, prms); var (adt, dt) = await GetQuery(connStr, dbSec, userAccountId, query, prms);
var page = new FuchsHtmlPage("", ""); var page = new FuchsHtmlPage("", "");
return await BuildFormHtmlAsync(adt, dt, qtype, FdsDestination.content, prms, page, query); return await BuildFormHtmlAsync(adt, dt, qtype, FdsDestination.content, prms, page, query);
} }
/// <summary>Renders a report as a full HTML page (destination = web / email), with optional caching.</summary> /// <summary>Renders a report as a full HTML page (destination = web / email), with optional caching.</summary>
public static async Task<FuchsHtmlPage> RenderPageAsync( public static async Task<FuchsHtmlPage> RenderPageAsync(
IntranetController ctrl, string uniquename, string query, FdsQueryType qtype, string connStr, DatabaseSecurity dbSec, string userAccountId,
string uniquename, string query, FdsQueryType qtype,
IDictionary<string, string> prms, FdsDestination dest, string templatePath, IDictionary<string, string> prms, FdsDestination dest, string templatePath,
bool allowcache = false, bool forceReload = false, string title = "") bool allowcache = false, bool forceReload = false, string title = "")
{ {
@@ -495,7 +497,7 @@ public static class FuchsVisualization
if (string.IsNullOrEmpty(cached)) if (string.IsNullOrEmpty(cached))
{ {
var start = DateTime.Now; var start = DateTime.Now;
var (adt, dt) = await GetQuery(ctrl, query, prms); var (adt, dt) = await GetQuery(connStr, dbSec, userAccountId, query, prms);
page.QueryDuration = (int)DateTime.Now.Subtract(start).TotalSeconds; page.QueryDuration = (int)DateTime.Now.Subtract(start).TotalSeconds;
// Title from the admin table when available // Title from the admin table when available
@@ -538,9 +540,10 @@ public static class FuchsVisualization
/// <summary>Renders a report query directly as a PNG chart.</summary> /// <summary>Renders a report query directly as a PNG chart.</summary>
public static async Task<byte[]?> RenderQueryAsChartAsync( public static async Task<byte[]?> RenderQueryAsChartAsync(
IntranetController ctrl, string query, FdsQueryType qtype, IDictionary<string, string> prms) string connStr, DatabaseSecurity dbSec, string userAccountId,
string query, FdsQueryType qtype, IDictionary<string, string> prms)
{ {
var (adt, dt) = await GetQuery(ctrl, query, prms); var (adt, dt) = await GetQuery(connStr, dbSec, userAccountId, query, prms);
if (adt == null || dt.Count == 0) return null; if (adt == null || dt.Count == 0) return null;
var cs = GetChartSettings(adt.Rows[0], dt[0]); var cs = GetChartSettings(adt.Rows[0], dt[0]);
-190
View File
@@ -1,190 +0,0 @@
using Fuchs.Controllers;
using Microsoft.AspNetCore.Mvc;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.SQL.sql;
using static OCORE.web.mvc_helper_async;
namespace Fuchs.intranet;
/// <summary>
/// Widget helpers for the Fuchs intranet dashboard.
/// Port of fuchs_fds_widgets.vb — SQL-driven widget cases.
/// Weather widget (wetter.com API) removed: API deprecated.
/// </summary>
public static class FuchsWidgets
{
public static async Task<IActionResult> IntranetWdg(IntranetController ctrl, string widgetId)
{
try
{
return widgetId.ToLower() switch
{
"my" => await HandleWidgetMy(ctrl),
"one" => await HandleWidgetOne(ctrl),
_ => await HandleWidgetGeneric(ctrl, widgetId)
};
}
catch (Exception ex)
{
ctrl._intranet.debug_log("FuchsWidgets.IntranetWdg", ex, ctrl.UserAccountID,
new { widgetId });
return new StatusCodeResult(500);
}
}
// ── "my" — list of widget short-names for the current user ───────────────
private static async Task<IActionResult> HandleWidgetMy(IntranetController ctrl)
{
var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser);",
ctrl._intranet.Intranet__SQLConnectionString,
ctrl.StdParamlist(SQL_VarChar("@account", "fis")),
Security: ctrl.DbSec);
var names = dt.DataTable.Rows
.Cast<System.Data.DataRow>()
.OrderBy(r => dt.DataTable.Columns.Contains("order") ? r.nz("order") : "")
.Select(r => r.nz("short_name"))
.ToArray();
return await JSONAsync(names);
}
// ── "one" — full widget data for a single widget ──────────────────────────
private static async Task<IActionResult> HandleWidgetOne(IntranetController ctrl)
{
string shortName = ctrl.Request.Form["short_name"].ToString() ?? "";
if (string.IsNullOrEmpty(shortName)) return new BadRequestResult();
var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_getONEPersonWidgets] (@authuser) WHERE [short_name] = @shortname;",
ctrl._intranet.Intranet__SQLConnectionString,
ctrl.StdParamlist(
SQL_VarChar("@shortname", shortName),
SQL_VarChar("@account", "fis")),
Security: ctrl.DbSec);
if (dt.Count != 1) return new StatusCodeResult(404);
var wdg = dt.FirstRow.toObjectDictionary();
return await BuildWidgetResponse(ctrl, shortName, wdg);
}
// ── Generic widget by id ──────────────────────────────────────────────────
private static async Task<IActionResult> HandleWidgetGeneric(IntranetController ctrl, string widgetId)
{
var pl = ctrl.StdParamlist(SQL_VarChar("@widget", widgetId, dbNull_IfEmpty: true));
var dset = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__getWidget] @widget, @authuser;",
ctrl._intranet.Intranet__SQLConnectionString, pl,
tablenames: new[] { "admin", "data" },
Security: ctrl.DbSec);
return await JSONAsync(new
{
admin = dset.Table("admin").FirstRow.toObjectDictionary(),
data = dset.Tables("data").toArrayofObjectDictionaries()
});
}
// ── Widget renderer dispatcher ────────────────────────────────────────────
private static async Task<IActionResult> BuildWidgetResponse(
IntranetController ctrl, string shortName, Dictionary<string, object?> wdg)
{
string dbType = (wdg.nz("type", "") ?? "").ToLower();
string sql = wdg.nz("sql", "") ?? "";
var ropts = ParseRenderingOptions(wdg.nz("rendering_options", "") ?? "");
string name = wdg.nz("name", "") ?? "";
string descr = wdg.nz("description", "") ?? "";
object widgetData;
switch (dbType)
{
case "sql_table":
{
var dt = await getSQLDatatable_async(sql,
ctrl._intranet.Intranet__SQLConnectionString,
ctrl.StdParamlist(), Security: ctrl.DbSec);
widgetData = new
{
name,
description = descr,
type = "table",
rendering_options = ropts,
columns = dt.DataTable.Columns
.Cast<System.Data.DataColumn>()
.Select(c => c.ColumnName)
.ToArray(),
data = dt.DataTable.Rows
.Cast<System.Data.DataRow>()
.Select(r => r.toObjectDictionary())
.ToArray()
};
break;
}
case "sql_indicator":
{
var dt = await getSQLDatatable_async(sql,
ctrl._intranet.Intranet__SQLConnectionString,
ctrl.StdParamlist(), Security: ctrl.DbSec);
var firstRow = dt.DataTable.Rows.Count > 0
? dt.DataTable.Rows[0].toObjectDictionary()
: new Dictionary<string, object?>();
widgetData = new
{
name,
description = descr,
type = "ind",
rendering_options = ropts,
data = new
{
status = firstRow.nz("status", "") ?? "",
value = firstRow.nz("value", "") ?? "",
label = firstRow.nz("label", "") ?? ""
}
};
break;
}
case "html":
widgetData = new
{
name,
description = descr,
type = "html",
rendering_options = ropts,
html = wdg.nz("html", "") ?? ""
};
break;
default:
// Pass through with normalised rendering_options
widgetData = new
{
name,
description = descr,
type = dbType,
rendering_options = ropts,
html = wdg.nz("html", "") ?? "",
url = wdg.nz("url", "") ?? "",
image = wdg.nz("image", "") ?? "",
data = (object)new
{
status = wdg.nz("status", "") ?? "",
value = wdg.nz("value", "") ?? "",
label = wdg.nz("label", "") ?? ""
}
};
break;
}
// Wrap under short_name key so JS can do response[wi]
return await JSONAsync(new Dictionary<string, object> { [shortName] = widgetData });
}
private static string[] ParseRenderingOptions(string raw) =>
string.IsNullOrWhiteSpace(raw)
? Array.Empty<string>()
: raw.Split(new[] { ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToArray();
}