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
-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 MigraDoc.DocumentObjectModel;
using MigraDoc.Rendering;
using Newtonsoft.Json.Linq;
using OCORE.SQL;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
using static OCORE.commons;
@@ -12,25 +7,26 @@ using static OCORE.commons;
namespace Fuchs.intranet;
/// <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>
public class FdsInvoiceData
{
private readonly JObject? _base;
private Document? _letter;
public GenericObjectDictionary? Admin { get; private set; }
public GenericObjectDictionary? NewValues { get; private set; }
public GenericObjectDictionary? Sms { get; private set; }
public List<Dictionary<string, object>>? Req { get; private set; }
public GenericObjectDictionary? InvoiceRegistration { get; private set; }
public bool IsDraft { get; private set; } = true;
public GenericObjectDictionary? InvoiceRegistration { get; internal set; }
public bool IsDraft { get; internal set; } = true;
public string Id => InvoiceRegistration?.getString("Id") ?? "";
public string PaymentTerms => InvoiceRegistration?.getString("PaymentTerm") ?? "";
// -- PDF-facing properties (used by FuchsPdf.ApplyInvoice) ----------------
// -- PDF-facing properties (used by IPdfService.ApplyInvoice) -------------
public string InvoiceType =>
InvoiceRegistration?.getString("InvoiceType").Substr(0, 1) ?? "R";
public string InvoiceId =>
@@ -56,7 +52,7 @@ public class FdsInvoiceData
{
IEnumerable<Dictionary<string, object?>>? itms =
itmsObj as IEnumerable<Dictionary<string, object?>>
?? (itmsObj is Newtonsoft.Json.Linq.JArray ja
?? (itmsObj is JArray ja
? ja.ToObject<List<Dictionary<string, object?>>>()
: null);
if (itms != null) result.AddRange(itms);
@@ -73,33 +69,33 @@ public class FdsInvoiceData
{
var result = new Dictionary<string, Dictionary<string, object?>>();
if (InvoiceRegistration == null) return result;
// Primary VAT slot
if (FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_1"), out decimal ust1)
&& FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_net1"), out decimal net1)
&& !(ust1 == 0 && net1 == 0))
result[ust1.ToString("0.##")] = new Dictionary<string, object?>
{ ["vat_amount"] = net1 };
// Secondary VAT slot
result[ust1.ToString("0.##")] = new Dictionary<string, object?> { ["vat_amount"] = net1 };
if (FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_2"), out decimal ust2)
&& FuchsPdf.ParseDec(InvoiceRegistration.getItem("InvoiceVAT_net2"), out decimal net2)
&& !(ust2 == 0 && net2 == 0))
result[ust2.ToString("0.##")] = new Dictionary<string, object?>
{ ["vat_amount"] = net2 };
result[ust2.ToString("0.##")] = new Dictionary<string, object?> { ["vat_amount"] = net2 };
return result;
}
}
// -- Raw properties --------------------------------------------------------
private string RawInvoiceAddress => NewValues?.nz("invoiceaddress") ?? "";
private string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? "";
private string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? "";
private string RawProvisionPeriod => NewValues?.nz("provisionperiod") ?? "";
private string[] RawProvisionLocation =>
// -- Raw form properties (used by parameter mapping) -----------------------
internal string RawInvoiceAddress => NewValues?.nz("invoiceaddress") ?? "";
internal string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? "";
internal string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? "";
internal string RawProvisionPeriod => NewValues?.nz("provisionperiod") ?? "";
internal string[] RawProvisionLocation =>
NewValues?.nz("provisionlocation") is { Length: > 0 } s
? s.Replace("\r\n", "\n").Split('\n').Select(t => t.Trim()).Where(t => t != "").ToArray()
: Array.Empty<string>();
// -- 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)
{
_base = ctd as JObject;
@@ -113,146 +109,16 @@ public class FdsInvoiceData
}
}
public FdsInvoiceData(string id, IntranetController ctrl) => RegisterInvoice(id, ctrl);
// -- 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)
// -- Parameter mapping (consumed by InvoiceService.RegisterInvoiceAsync) ---
internal List<SqlParameter> BuildInvoiceParams(bool change, string invId)
{
_ = change; _ = invId;
var vatsDic = new Dictionary<string, string>();
if (Req != null)
{
foreach (var rq in Req)
{
if (rq.TryGetValue("items", out var itmsObj) &&
itmsObj is List<object> itms)
if (rq.TryGetValue("items", out var itmsObj) && itmsObj is List<object> itms)
{
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 OCORE.SQL;
using static OCORE.OCORE_dictionaries;
using static OCORE.SQL.sql;
using static OCORE.commons;
namespace Fuchs.intranet;
/// <summary>
/// Encapsulates a reminder (Zahlungserinnerung) data object.
/// Converted from VB fds__reminder_data class.
/// Reminder (Zahlungserinnerung) data holder. Converted from VB fds__reminder_data.
/// Pure data — persistence and PDF generation live in
/// <see cref="Fuchs.Services.IReminderService"/> (no controller coupling).
/// </summary>
public class FdsReminderData
{
private readonly JObject? _base;
private Document? _letter;
public GenericObjectDictionary? NewValues { get; private set; }
public GenericObjectDictionary? Rem { get; private set; }
public GenericObjectDictionary? ReminderRegistration { get; private set; }
public bool IsDraft { get; private set; } = true;
public GenericObjectDictionary? ReminderRegistration { get; internal set; }
public bool IsDraft { get; internal set; } = true;
public string Id => ReminderRegistration?.getString("Id") ?? "";
// -- 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")
.Replace("\r\n", "\n").Replace("\n\n", "\n").Split('\n')
.Select(t => System.Web.HttpUtility.HtmlDecode(t.Trim())).Where(t => t != "").ToArray()
: Array.Empty<string>();
public string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? "";
public string RawInvId => Rem?.nz("invid").ne(Rem?.nz("InvId") ?? "").Trim() ?? "";
public string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? "";
internal string RawInvoiceEmail => NewValues?.nz("invoiceemail").Trim() ?? "";
internal string RawInvId => Rem?.nz("invid").ne(Rem?.nz("InvId") ?? "").Trim() ?? "";
internal string RawCustomValues => NewValues?.nz("CustomValues").Trim() ?? "";
// -- Computed props from registration -------------------------------------
public DateTime? DateCreated => ReminderRegistration?.getString("DateCreated") is { Length: > 0 } d ? DateTime.Parse(d) : null;
@@ -59,11 +52,10 @@ public class FdsReminderData
get
{
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");
try
{
if (raw is Newtonsoft.Json.Linq.JArray ja)
if (raw is JArray ja)
return ja.ToObject<List<Dictionary<string, object?>>>() ?? new();
if (raw is string s && s.StartsWith("["))
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object?>>>(s) ?? new();
@@ -74,6 +66,10 @@ public class FdsReminderData
}
// -------------------------------- 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)
{
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>>()!);
}
}
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.Text;
using System.Text.RegularExpressions;
using Fuchs.Controllers;
using HtmlAgilityPack;
using Microsoft.Data.SqlClient;
using Newtonsoft.Json;
using OCORE.GenericCharts;
using OCORE.security;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.SQL.sql;
@@ -177,11 +177,11 @@ internal sealed class ManagedCache
public static class FuchsVisualization
{
// ── 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>
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>();
DataTable? adt = null;
@@ -192,10 +192,10 @@ public static class FuchsVisualization
var reportinfo = await getSQLDataSet_async(
"EXECUTE [dbo].[fds__admin_getReportCatalog] @report_name, @authuser;",
ConnStr(ctrl),
new List<SqlParameter> { SQL_VarChar("@report_name", query), SQL_VarChar("@authuser", ctrl.UserAccountID) },
connStr,
new List<SqlParameter> { SQL_VarChar("@report_name", query), SQL_VarChar("@authuser", userAccountId) },
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)
return (adt, dtList);
@@ -204,8 +204,8 @@ public static class FuchsVisualization
var procRow = reportinfo.Tables("procedures").Rows[0];
string sql = $"EXECUTE [dbo].[{procRow["name"]}] {procRow["parameter"]};";
var dset = await getSQLDataSet_async(sql, ConnStr(ctrl), qparams,
Security: ctrl.DbSec, options: new FIS_SQLOptions());
var dset = await getSQLDataSet_async(sql, connStr, qparams,
Security: dbSec, options: new FIS_SQLOptions());
if (isXls)
{
@@ -456,16 +456,18 @@ public static class FuchsVisualization
/// <summary>Renders a report as a bare HTML fragment (destination = content).</summary>
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("", "");
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>
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,
bool allowcache = false, bool forceReload = false, string title = "")
{
@@ -495,7 +497,7 @@ public static class FuchsVisualization
if (string.IsNullOrEmpty(cached))
{
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;
// 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>
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;
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();
}