Initial Commit after switching from SVN to git

This commit is contained in:
2026-05-03 01:43:52 +02:00
parent ab8638e5bb
commit a4284234b2
910 changed files with 359931 additions and 0 deletions
BIN
View File
Binary file not shown.
+125
View File
@@ -0,0 +1,125 @@
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;
}
}
+288
View File
@@ -0,0 +1,288 @@
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 invoice (Rechnung) data. Converted from VB fds__invoice_data class.
/// </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 string Id => InvoiceRegistration?.getString("Id") ?? "";
public string PaymentTerms => InvoiceRegistration?.getString("PaymentTerm") ?? "";
// -- PDF-facing properties (used by FuchsPdf.ApplyInvoice) ----------------
public string InvoiceType =>
InvoiceRegistration?.getString("InvoiceType").Substr(0, 1) ?? "R";
public string InvoiceId =>
InvoiceRegistration?.getString("InvoiceId") ?? Id;
public string InvoiceTitle =>
InvoiceRegistration?.getString("InvoiceTitle") ?? "";
public string[] ProvisionLocation =>
InvoiceRegistration?.getString("ProvisionLocation") is { Length: > 0 } pl
? pl.Replace("<br>", "\n").Replace("<br/>", "\n").Replace("<br />", "\n")
.Replace("\r\n", "\n").Split('\n').Select(t => t.Trim()).Where(t => t != "").ToArray()
: RawProvisionLocation;
/// <summary>Flat list of line items from all nested service request groups.</summary>
public List<Dictionary<string, object?>> InvoiceItems
{
get
{
var result = new List<Dictionary<string, object?>>();
if (Req == null) return result;
foreach (var req in Req)
{
if (req.TryGetValue("items", out var itmsObj))
{
IEnumerable<Dictionary<string, object?>>? itms =
itmsObj as IEnumerable<Dictionary<string, object?>>
?? (itmsObj is Newtonsoft.Json.Linq.JArray ja
? ja.ToObject<List<Dictionary<string, object?>>>()
: null);
if (itms != null) result.AddRange(itms);
}
}
return result;
}
}
/// <summary>VAT rows keyed by percentage string (e.g. "19"), from InvoiceRegistration.</summary>
public Dictionary<string, Dictionary<string, object?>> VatRows
{
get
{
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
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 };
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 =>
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 -----------------------------------------------------------------
public FdsInvoiceData(object ctd)
{
_base = ctd as JObject;
IsDraft = true;
if (_base != null)
{
if (_base.ContainsKey("admin")) Admin = new GenericObjectDictionary(_base["admin"]!.ToObject<Dictionary<string, object>>()!);
if (_base.ContainsKey("new")) NewValues = new GenericObjectDictionary(_base["new"]!.ToObject<Dictionary<string, object>>()!);
if (_base.ContainsKey("sms")) Sms = new GenericObjectDictionary(_base["sms"]!.ToObject<Dictionary<string, object>>()!);
if (_base.ContainsKey("req")) Req = _base["req"]!.ToObject<List<Dictionary<string, object>>>();
}
}
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)
{
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)
{
foreach (var itm in itms.OfType<Dictionary<string, object?>>())
{
string vatKey = itm.nz("vat", "").Replace("%", "").Trim();
if (!string.IsNullOrEmpty(vatKey) && vatKey != "0")
vatsDic.TryAdd(vatKey, vatsDic.TryGetValue(vatKey, out var ve) ? ve : "0");
}
}
}
}
string vathigh = vatsDic.Keys.OrderByDescending(k => double.TryParse(k, out var d) ? d : 0).FirstOrDefault() ?? "19";
return new List<SqlParameter>
{
SQL_VarChar("@InvoiceType", Admin?.nz("type") ?? ""),
SQL_NVarChar("@InvoiceTitle", NewValues?.nz("title") ?? ""),
SQL_Float("@InvoiceBalance", stringvalue: NewValues?.nz("total_gross") ?? "0"),
SQL_Float("@InvoiceBalance_net", stringvalue: NewValues?.nz("total_net") ?? "0"),
SQL_Float("@InvoiceVAT_net1", stringvalue: NewValues?.nz($"vat_{vathigh}_net") ?? "0"),
SQL_VarChar("@InvoiceVAT_1", vathigh),
SQL_VarChar("@PaymentTerm", NewValues?.nz("paymentterm") ?? "", dbNull_IfEmpty: true),
SQL_BigInt("@CustomerId", Admin?.nz("customerid") ?? ""),
SQL_VarChar("@SendToAddress", RawInvoiceAddress),
SQL_NVarChar("@SendToEmail", RawInvoiceEmail),
SQL_NVarChar("@ProvisionPeriod", RawProvisionPeriod, dbNull_IfEmpty: true),
SQL_NVarChar("@CustomValues", RawCustomValues, dbNull_IfEmpty: true),
SQL_Float("@InvoiceService_net", stringvalue: Sms?.nz("tscn") ?? "0"),
SQL_Float("@InvoiceService_VAT", stringvalue: Sms?.nz("tscvat") ?? "0"),
SQL_VarChar("@InvoiceOptions",
Admin?.no("p13b", false) is true ? "§13b" : "", dbNull_IfEmpty: true)
};
}
}
+236
View File
@@ -0,0 +1,236 @@
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.
/// </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 string Id => ReminderRegistration?.getString("Id") ?? "";
// -- Raw props from form data ---------------------------------------------
public 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() ?? "";
// -- Computed props from registration -------------------------------------
public DateTime? DateCreated => ReminderRegistration?.getString("DateCreated") is { Length: > 0 } d ? DateTime.Parse(d) : null;
public string ReminderType => ReminderRegistration?.getString("type").Substr(0, 1) ?? "";
public string UserNameFinalized => ReminderRegistration?.getString("UserNameFinalized") ?? "";
public string UserEmailFinalized => ReminderRegistration?.getString("UserEmailFinalized") ?? "";
public string InvoiceId => ReminderRegistration?.getString("InvoiceId") ?? "";
public string[] InvoiceAddress => ReminderRegistration?.getString("SendToAddress") 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 => t.Trim()).ToArray()
: Array.Empty<string>();
public string InvoiceEmail => ReminderRegistration?.getString("SendToEmail") ?? "";
public string ReminderTitle => ReminderRegistration?.getString("subject") ?? "";
public string Subject => ReminderTitle;
/// <summary>List of invoice items referenced by this reminder (from registration data).</summary>
public List<Dictionary<string, object?>> ReminderItems
{
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)
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();
}
catch { }
return new List<Dictionary<string, object?>>();
}
}
// -------------------------------- Ctors ----------------------------------
public FdsReminderData(object ctd)
{
if (ctd is JObject jo) { _base = jo; }
IsDraft = true;
if (_base != null)
{
if (_base.ContainsKey("new")) NewValues = new GenericObjectDictionary(_base["new"]!.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;
}
}
+196
View File
@@ -0,0 +1,196 @@
using Cfg = System.Configuration.ConfigurationManager;
using Fuchs.intranet;
using MimeKit;
using Newtonsoft.Json;
using Microsoft.Data.SqlClient;
using OCORE.SQL;
using static OCORE.SQL.sql;
namespace Fuchs.intranet;
/// <summary>
/// Email sending helper for Fuchs intranet. Full port of fuchs_fds_email.vb.
/// Uses OCORE.email.Emailcommons.SendEmail_async when settings are available,
/// falls back to MailKit direct send otherwise.
/// </summary>
public static class FuchsFdsEmail
{
private static OCORE.email.EmailServerSettings? _settings;
private static OCORE.email.EmailServerSettings? GetEmailSettings()
{
if (_settings != null) return _settings;
string raw = Cfg.AppSettings["FDS_EmailSettings"] ?? "";
if (string.IsNullOrWhiteSpace(raw)) return null;
// The serialised string is JSON containing a "type" field
string type = "smtp";
try
{
var dic = JsonConvert.DeserializeObject<Dictionary<string, object>>(raw);
if (dic?.TryGetValue("type", out var t) == true)
type = t?.ToString() ?? "smtp";
}
catch { /* keep default type */ }
_settings = new OCORE.email.EmailServerSettings(type, raw);
return _settings;
}
private const string ReplyToName = "Sebastian Fuchs - Bad und Heizung";
private const string ReplyToAddress = "info@sanitaerfuchs.de";
private const string SignatureIntro =
"<p>&nbsp;</p><p style=\"margin:24px 0 16px 0;line-height:140%;\">" +
"Herzliche Gr\u00fc\u00dfe aus D\u00fcsseldorf-Bilk<br/>" +
"Ihr Team der Firma Sebastian Fuchs</p>";
/// <summary>
/// Sends an email and logs the result to the Fuchs FDS database.
/// Returns true on success.
/// </summary>
public static async Task<bool> SendEmail(
string @ref,
string subject,
string html,
string email,
string name,
Dictionary<string, byte[]>? files,
Fuchs_intranet intranet)
{
var errors = new List<string>();
string guid = "";
string config = "";
DateTime sent = default;
if (!IsValidEmail(email))
errors.Add("Die Email-Adresse ist nicht g\u00fcltig.");
if (string.IsNullOrEmpty(html))
errors.Add("Bitte geben Sie eine Nachricht ein.");
if (errors.Count == 0)
{
try
{
var settings = GetEmailSettings();
string body = html + BuildSignature();
if (settings != null)
{
// ── OCORE path ─────────────────────────────────────────
var msg = new OCORE.email.Email(
Mode: OCORE.email.EmailMode.DirectMode,
type: settings.type)
{
EmailSettings = settings,
Subject = subject
};
msg.AddTo(email, name);
msg.AddReplyTo(new MailboxAddress(ReplyToName, ReplyToAddress));
msg.SetBody(body);
if (files != null)
foreach (var kv in files)
msg.AttachFile(filecontent: kv.Value, kv.Key);
var result = await msg.SendAsync(
(dref, ex) => intranet.debug_log(
$"FuchsFdsEmail.SendEmail {dref}", ex));
guid = msg.MessageId ?? "";
config = msg.EmailConfig_serialized;
sent = result.Timestamp;
errors.AddRange(result.ErrorMessages);
}
else
{
// ── MailKit fallback (app settings) ────────────────────
string host = Cfg.AppSettings["smtp_host"] ?? "";
string user = Cfg.AppSettings["smtp_user"] ?? "";
string pass = Cfg.AppSettings["smtp_pass"] ?? "";
string from = Cfg.AppSettings["smtp_from"] ?? user;
string fromName = Cfg.AppSettings["smtp_fromname"] ?? "Sanit\u00e4rFuchs";
int port = int.TryParse(Cfg.AppSettings["smtp_port"], out int p) ? p : 587;
var message = new MimeMessage();
message.From.Add(new MailboxAddress(fromName, from));
message.To.Add(new MailboxAddress(name, email));
message.ReplyTo.Add(new MailboxAddress(ReplyToName, ReplyToAddress));
message.Subject = subject;
var builder = new BodyBuilder { HtmlBody = body };
if (files != null)
foreach (var kv in files)
builder.Attachments.Add(kv.Key, kv.Value);
message.Body = builder.ToMessageBody();
using var client = new MailKit.Net.Smtp.SmtpClient();
await client.ConnectAsync(host, port,
MailKit.Security.SecureSocketOptions.Auto);
if (!string.IsNullOrEmpty(user))
await client.AuthenticateAsync(user, pass);
await client.SendAsync(message);
await client.DisconnectAsync(true);
guid = message.MessageId!;
sent = DateTime.UtcNow;
}
}
catch (Exception ex)
{
errors.Add("Beim Versenden ist ein Fehler aufgetreten.");
intranet.debug_log("FuchsFdsEmail.SendEmail inner", ex, data: errors);
}
}
if (errors.Count > 0)
intranet.debug_log("FuchsFdsEmail.SendEmail",
data: new { @ref, email, errors });
// ── SQL audit log ──────────────────────────────────────────────────
try
{
var pl = new List<SqlParameter>
{
SQL_VarChar("@Ref", @ref),
SQL_VarChar("@guid", guid),
SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent),
SQL_NVarChar("@config", config, dbNull_IfEmpty: true),
SQL_Bit("@success", errors.Count == 0),
SQL_NVarChar("@log", JsonConvert.SerializeObject(errors))
};
await setSQLValue_async(
"EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;",
intranet.Intranet__SQLConnectionString, pl,
Security: intranet.GetDbSecurity());
}
catch (Exception logEx)
{
intranet.debug_log("FuchsFdsEmail.SendEmail log", logEx);
}
return errors.Count == 0;
}
// ── Helpers ────────────────────────────────────────────────────────────────
private static string BuildSignature()
{
try
{
string sigPath = Path.Combine(AppContext.BaseDirectory,
"email_signature", "sanitaerfuchs_email_signature.txt");
if (File.Exists(sigPath))
return SignatureIntro + File.ReadAllText(sigPath);
}
catch { /* signature is optional */ }
return "";
}
private static bool IsValidEmail(string email)
{
try
{
var a = new System.Net.Mail.MailAddress(email);
return string.Equals(a.Address, email, StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
}
+233
View File
@@ -0,0 +1,233 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Data.SqlClient;
using OCORE.security;
using OCORE.SQL;
using static OCORE.commons;
using static OCORE.SQL.sql;
namespace Fuchs.intranet;
// --------------------------- Global singleton --------------------------------
/// <summary>Global singleton holder for the Fuchs intranet instance.</summary>
public static class FuchsOcmsIntranet
{
private static Fuchs_intranet? _instance;
private static readonly object _lock = new();
public static Fuchs_intranet Instance
{
get
{
if (_instance == null)
throw new InvalidOperationException("FuchsOcmsIntranet has not been initialized. Call Initialize() in Program.cs.");
return _instance;
}
}
public static void Initialize(IConfiguration configuration)
{
if (_instance == null)
lock (_lock) { _instance ??= new Fuchs_intranet(configuration); }
}
}
// --------------------------- Configuration -----------------------------------
/// <summary>
/// Fuchs-specific intranet configuration and shared services.
/// Replaces the old VB OCMS_intranet subclass.
/// </summary>
public class Fuchs_intranet
{
public static readonly CultureInfo DeCulture = new("de-DE");
public const string AuthScheme = "fuchs_intranet";
private readonly IConfiguration _config;
public Fuchs_intranet(IConfiguration configuration)
{
_config = configuration;
}
// -- Connection / security ------------------------------------------------
public string Intranet__SQLConnectionString =>
_config.GetConnectionString("fuchs_fds_ConnectionString")
?? throw new InvalidOperationException("Missing connection string: fuchs_fds_ConnectionString");
public NamePasswordPair Intranet__sql_symmetric_key =>
new() { Name = "fuchs_enc_1", Password = "*0&PY_6iyjlKyQ" };
public DatabaseSecurity GetDbSecurity(string? userId = null) =>
new(userId, Intranet__sql_symmetric_key);
// -- App settings ---------------------------------------------------------
public string Intranet__SMS_API_key =>
_config["Fuchs:SMS_APIKey"] ?? "";
public string Intranet__TOTPsharedsecret_base =>
_config["Fuchs:fuchs_intranet_TOTP"] ?? "";
public string Intranet__cookiename =>
"fuchs_" + (_config["Fuchs:fuchs_intranet_guid"] ?? "intranet");
public bool FDS_Intranet_DebugState =>
_config.GetValue<bool>("Fuchs:FDS_Intranet_DebugState");
/// <summary>When true (dev only), automatically signs in <see cref="DevAutoLoginEmail"/> on localhost.</summary>
public bool DevAutoLogin =>
_config.GetValue<bool>("Fuchs:DevAutoLogin");
/// <summary>The email address used for auto-login in development (requires <see cref="DevAutoLogin"/> = true).</summary>
public string DevAutoLoginEmail =>
_config["Fuchs:DevAutoLoginEmail"] ?? "";
// -- SQL connection helper -------------------------------------------------
public SqlConnection Intranet_SqlCon()
{
var con = new SqlConnection(Intranet__SQLConnectionString);
con.Open();
return con;
}
// -- Authentication helpers ------------------------------------------------
/// <summary>Authenticates a user by email/password against the Fuchs SQL database.</summary>
public async Task<System.Data.DataRow?> AuthenticateAsync(string email, string password)
{
try
{
var pl = new List<SqlParameter>
{
SQL_VarChar("@email", email),
SQL_NVarChar("@password", password)
};
var dt = await getSQLDatatable_async(
"SELECT * FROM [dbo].[fis_admin_authenticate](@email, @password);",
Intranet__SQLConnectionString, pl, Security: GetDbSecurity());
return dt.Count > 0 ? dt.FirstRow : null;
}
catch { return null; }
}
/// <summary>Loads user account row by email.</summary>
public async Task<System.Data.DataRow?> GetUserAccountByEmailAsync(string email, bool includePassword = false)
{
var pl = new List<SqlParameter>
{
SQL_VarChar("@email", email),
SQL_Bit("@include_password", includePassword)
};
var dt = await getSQLDatatable_async(
"SELECT TOP(1) * FROM [dbo].[fis_admin_getUserAccount_byemail](@email, @include_password);",
Intranet__SQLConnectionString, pl, Security: GetDbSecurity());
return dt.Count > 0 ? dt.FirstRow : null;
}
/// <summary>Loads user account row by account ID.</summary>
public async Task<System.Data.DataRow?> GetUserAccountAsync(string userAccountId)
{
var pl = new List<SqlParameter> { SQL_VarChar("@useraccount_id", userAccountId) };
var dt = await getSQLDatatable_async(
"SELECT TOP(1) * FROM [dbo].[fis_admin_getUserAccount](@useraccount_id);",
Intranet__SQLConnectionString, pl, Security: GetDbSecurity(userAccountId));
return dt.Count > 0 ? dt.FirstRow : null;
}
/// <summary>Gets module authorization level for a user.</summary>
public async Task<int> GetModuleAuthAsync(string authModule, string userAccountId)
{
var pl = new List<SqlParameter>
{
SQL_VarChar("@authuser", userAccountId),
SQL_VarChar("@module", authModule, dbNull_IfEmpty: true)
};
var val = await getSQLValue_async<int>(
"SELECT ISNULL([dbo].[fis_getModuleAuth](@module, @authuser), -1);",
Intranet__SQLConnectionString, -1, pl, Security: GetDbSecurity(userAccountId));
return val.Exception != "" ? -1 : val.Result;
}
// -- Logging ---------------------------------------------------------------
public void debug_log(string procedure, Exception? ex = null, string authuser = "", object? data = null)
{
try
{
var pl = new List<SqlParameter>
{
SQL_VarChar("@CodeReference", procedure.Left(200)),
SQL_NVarChar("@ExceptionMessage", ex?.Message ?? "", dbNull_IfEmpty: true),
SQL_NVarChar("@StackTrace", ex?.StackTrace ?? "", dbNull_IfEmpty: true),
SQL_NVarChar("@Data", data != null ? Newtonsoft.Json.JsonConvert.SerializeObject(data) : "", dbNull_IfEmpty: true)
};
string dbEx = ""; int? dbCode = null;
setSQLValue("EXECUTE [dbo].[fds__admin_logdebug] @CodeReference, @ExceptionMessage, @StackTrace, @Data;",
Intranet_SqlCon(), ref dbEx, ref dbCode, pl, Security: GetDbSecurity(authuser));
}
catch { /* swallow logging errors */ }
}
// -- PDF license -----------------------------------------------------------
public static void SetPdfLicense()
{
try
{
var licPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Spire.License.dll");
if (File.Exists(licPath))
Spire.License.LicenseProvider.SetLicenseFileFullPath(licPath);
}
catch { }
}
}
// --------------------------- User identity -----------------------------------
/// <summary>Represents the authenticated Fuchs intranet user extracted from claims.</summary>
public class FuchsUserIdentity
{
public static readonly string ClaimUserAccountId = "fuchs:useraccount_id";
public static readonly string ClaimEmail = "fuchs:email";
public static readonly string ClaimAuthorization = "fuchs:authorization";
public string UserAccountId { get; }
public string Email { get; }
public int Authorization { get; }
public bool IsAuthenticated => !string.IsNullOrEmpty(UserAccountId);
public FuchsUserIdentity(ClaimsPrincipal? principal)
{
UserAccountId = principal?.FindFirstValue(ClaimUserAccountId) ?? "";
Email = principal?.FindFirstValue(ClaimEmail) ?? "";
Authorization = int.TryParse(principal?.FindFirstValue(ClaimAuthorization), out var a) ? a : 0;
}
public static ClaimsIdentity BuildIdentity(
string userAccountId, string email, int authorization, string authScheme) =>
new(new[]
{
new Claim(ClaimUserAccountId, userAccountId),
new Claim(ClaimEmail, email),
new Claim(ClaimAuthorization, authorization.ToString())
}, authScheme);
}
// --------------------------- SQL options -------------------------------------
/// <summary>
/// Fuchs-specific SQL options — adds debug logging on error.
/// </summary>
public class FIS_SQLOptions : sqloptions
{
public FIS_SQLOptions(Dictionary<string, object>? context = null)
{
OnError = (procedure, ex, data) =>
{
try { FuchsOcmsIntranet.Instance.debug_log($"SQL Error in {procedure}", ex, data: context); }
catch { }
};
}
}
+733
View File
@@ -0,0 +1,733 @@
using System.Globalization;
using System.Text.RegularExpressions;
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Shapes;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Extensions;
using MigraDoc.Extensions.Html;
using MigraDoc.Rendering;
using PdfSharp.Drawing;
using QRCoder;
using static OCORE.commons;
namespace Fuchs.intranet;
/// <summary>
/// PDF generation helpers for the Fuchs intranet.
/// Full port of fuchs_fds_pdf.vb.
/// </summary>
public static class FuchsPdf
{
public static readonly CultureInfo DeCulture = new("de-DE");
public const string ProjectAbbreviation = "fuchs";
// ── Spire license ─────────────────────────────────────────────────────────
public static void SetLicense() =>
Spire.License.LicenseProvider.SetLicenseKey(
"I+ztXu/77JVCXwEAwVQwRISgL4qlo1lOxO6csGdd02iJsOnMzEkqjhRx6oJ5rw5fgaF5wUf83LWMWwLE8PNc" +
"/ZGUZIa8mTx9ovjM9fK2+xLk/VC3s555Qhd5+PLfgxIEsp4r6lw03P7YPvD6pvM745VQg0dd8thRoznmkWrkUf" +
"/2/MiUZyUyVrH+qyEZgkniqpuDdqoaUNx1RfsK6TyiKKB7nsiqDy9xrduuYCMgOg1wii3aU+anA/pHUYh/jMO0" +
"TkavDRxzlL2GpijSDzIte0eXCR6K8GXLOpV3HKYnjErkiIn5zPIp9v7IzM+55FSiC3kAYwmxhMCH8M/eTZ6/2qh" +
"CYQXSbmX0h9ET3EMmhgj5lbuU84YAQNKuj+lIohxSDylTyYBekGTXP1fUud14pMqP2QrCkbYo7INgMkozZu/3q7" +
"r7lKOhQAPhzT31eL+63Br2c6NIC+y6y6cOUhz2jBE4trJtBXzNMwotfO2VtZ2tQYPuzyFylwcf2V7wULsM888p8" +
"kW9XeD+oKm/0UYR0IbS/S/t6HSuY7rS/zvXdcObvH11sGRcvjko7Pl7xY9xHGj68RAyd0uJlVyV/dY8om3llxM8" +
"5F+mPYS5QHRmHerl4DyiQSvhkHBajLZ2MYG3Lk5qtFlVuIRLL3ohTEUdRJoW3JmOsJDSFG2f1zgwQUbhF29wwnf" +
"EPyfkVuxoMzExqPl2/LC/vpxQpzES0leMdW0KGkcvksHPiQd6aAqYPBTHZ+0aQoD8MHctMVdQHiQyWAiRVfljpt9" +
"fcOh24zNBRNijnUm0EN/QO8nZZ4pMqAU87U4mWLlyWBXqgmw//M3yEaXHEsYt/3RIr6yERpyQGVYUvoIVdXrOHv" +
"watclsbir0qMndh9q36C0Jwk0E7GK5HpfOzNreoNUxKJHbQmgN7xe5POyBCizhT9Tt2QGMX3YqlhgWLjCSrJvR1" +
"59pbdtyo8WJETG49Ba7w7Ll+evakE5Tpzidtnrm0uy5mEgUWUQa4N/FCJuMYOpyuVXDAz5kTiotgg9Xo5rFxUH+" +
"xufUadkE8brQ3e/y1MUxYw02CQaBimZUX/LCMRhL4u89lBkGTJOqLXsxEAXV5OTVtqAqsq06IT6MlpVjt4Dh1eJ" +
"xH8bz6IJEJYjEgam0FVHkTk3kfOf95+QcsvVdHFXsdGszFMnAqX6b8nKcAnZvZWpWYuUtEbOsyuMTo6io8XNwKhp" +
"uUg5LlJmPPXkTKHQJ/CM6EQkqIS4Foz7pBaaYRBgEz/zDujxbYUGN6LaJiANung4Zyl6k5arhHdCalRDe29avN1o" +
"vxe/5tUHQQDxq+yQ1cNChPJTFHR1bKKu0T7SW7p19qH5850rXcjtzK4+6zGYXq8HItH6UNiev27o9VUoKTv+XZiD" +
"27YE33vdwQHh5Kdc8CMMo+uaTI11uLBirUH63Na2oBkCGJjJzQk8Gc5NQs7+2DptJ/rNlOhwb/czZLB6OjH+vNCy" +
"HZBCGPd17rIW16JQzgWv+OBI9DbD7pXYzDyF++IrBiRKBPNKCTwg3trm89J4zWeGW80bFtD0QnIcArA==");
// ── Colors ────────────────────────────────────────────────────────────────
private static readonly Color FuchsGray = Color.FromRgb(128, 128, 128);
private static readonly Color FuchsBlau = Color.FromRgb(27, 66, 120);
// ── Unit helpers ──────────────────────────────────────────────────────────
public static Unit cm(double v) => Unit.FromCentimeter(v);
public static Unit mm(double v) => Unit.FromMillimeter(v);
public static Unit pt(double v) => Unit.FromPoint(v);
// ── Shading helpers ───────────────────────────────────────────────────────
public static Shading WhiteShading() => new() { Color = Colors.White, Visible = false };
public static Shading LightShading() => new() { Color = Color.FromRgb(240,240,240), Visible = true };
public static Shading GrayShading() => new() { Color = Color.FromRgb(235,235,235), Visible = true };
// ── Text block model ──────────────────────────────────────────────────────
public class FdsTextBlocks
{
public string OrtUndZeit { get; set; } = "";
public string[] Address { get; set; } = Array.Empty<string>();
public string AdminRef { get; set; } = "";
public string AdminUser { get; set; } = "Stefan Ott";
public string AdminUserEmail { get; set; } = "info@processweb.de";
public string AdminDatumLabel { get; set; } = "Rechnungsdatum";
public DateTime AdminDatumValue { get; set; } = DateTime.Now;
public string AdminDatum => AdminDatumValue.ToString("dd. MMMM yyyy", DeCulture);
public string AdminProvLabel { get; set; } = "Leistungszeitraum";
public string ProvisionPeriod { get; set; } = "";
public string Subject { get; set; } = "";
public string Body { get; set; } = "";
public string SenderLine1 { get; set; } = "Sebastian Fuchs";
public string SenderLine2 { get; set; } = "GmbH & Co. KG \u25cf Germaniastra\u00dfe 15 \u25cf 40223 D\u00fcsseldorf";
public string ProvisionLoc_Label { get; } = "Leistungsort / Lieferadresse";
public string DocInfo_Author { get; } = "Sebastian Fuchs Sanit\u00e4r und Heizung, D\u00fcsseldorf";
public string[] FooterBlock1 { get; } = new[]
{
"Sebastian Fuchs", "Bad und Heizung GmbH \u0026 Co. KG", "Sitz: Germaniastr. 15",
"40223 D\u00fcsseldorf", "Amtsgericht D\u00fcsseldorf HRA22282", "USt-ID-Nr.: DE286366012"
};
public string[] FooterBlock2 { get; } = new[]
{
"Pers\u00f6nlich haftend:", "Sebastian Fuchs Verwaltungs GmbH",
"Amtsgericht D\u00fcsseldorf HRB69289", "Gesch\u00e4ftsf\u00fchrer: Sebastian Fuchs",
"Installateur und Heizungsbaumeister", "Energieberater HWK"
};
public string[] FooterBlock3 { get; } = new[]
{
"Telefon: 0211 - 31 07 222", "Fax: 0211 - 87 66 185",
"Notdienst: 0177 - 88 08 167", "E-Mail: info@sanitaerfuchs.de", "Website: www.sanitaerfuchs.de"
};
public string[] FooterBlock4 { get; } = new[]
{
"Kreissparkasse D\u00fcsseldorf", "IBAN: DE52 3015 0200 0002 0914 78", "BIC: WELADED1KSD",
"Stadtsparkasse D\u00fcsseldorf", "IBAN: DE76 3005 0110 0045 0148 00", "BIC: DUSSDEDDXXX"
};
public Dictionary<string, string[]> ReminderTexts_before { get; } = new()
{
["f"] = new[] {
"Sehr geehrte Damen und Herren,",
"wir erlauben uns, Sie auf die nachfolgende offene Rechnung aufmerksam zu machen."
},
["s"] = new[] {
"Sehr geehrte Damen und Herren,",
"trotz unserer Ersten Mahnung haben wir bis heute noch keinen Zahlungseingang feststellen k\u00f6nnen. " +
"Wir bitten Sie, den offenen Betrag inkl. der Mahnzinsen und Mahnkosten umgehend auf unser Konto zu \u00fcberweisen."
},
["l"] = Array.Empty<string>()
};
}
// ── Currency / parse helpers ───────────────────────────────────────────────
public static string Currency(object? input, string returnobject = "?")
{
if (input == null || input is DBNull) return returnobject;
if (decimal.TryParse(input.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
return d.ToString("0.00 \u20ac", DeCulture);
return returnobject;
}
public static string TranslatePaymentTerm(string pt) =>
pt.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen");
public static bool ParseDec(object? input, out decimal val)
{
val = 0;
return input != null && decimal.TryParse(
input.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out val);
}
// ── HTML split helper (for multi-line PDF cells) ──────────────────────────
public static string[] HtmlSplit(string? inp, int maxCharsPerRow)
{
string s = inp ?? "";
if (s.Length <= maxCharsPerRow || !s.Contains('<')) return new[] { s };
var matches = Regex.Matches(s, "<[p,div]");
if (matches.Count == 0) return new[] { s };
var parts = new List<string>();
int lastIndex = 0;
string descStr = "<div class=\"desc\">";
int descPos = s.IndexOf(descStr, StringComparison.Ordinal);
if (descPos >= 0)
{
parts.Add(s[..descPos]);
s = s[(descPos + descStr.Length - 6)..];
matches = Regex.Matches(s, "<[p,div]");
}
foreach (Match m in matches)
if (m.Index > 0 && (m.Index - lastIndex) > maxCharsPerRow)
{
parts.Add(s[lastIndex..m.Index]);
lastIndex = m.Index;
}
parts.Add(s[lastIndex..]);
return parts.ToArray();
}
// ── QR payment code ───────────────────────────────────────────────────────
public static System.Drawing.Bitmap GetPaycode(
string iban, string bic, string name, decimal amount, string purpose)
{
string payload =
$"BCD\n002\n1\nSCT\n{bic}\n{name}\n{iban}\nEUR{amount:F2}\n\n\n{purpose}";
using var gen = new QRCodeGenerator();
using var data = gen.CreateQrCode(payload, QRCodeGenerator.ECCLevel.M);
using var qr = new QRCode(data);
return qr.GetGraphic(10);
}
// ── Page builders ─────────────────────────────────────────────────────────
public static Section CreatePage_Empty(Document doc,
double topMargin = 0, double bottomMargin = 0,
double leftMargin = 0, double rightMargin = 0)
{
var section = doc.AddSection();
section.PageSetup.TopMargin = cm(topMargin);
section.PageSetup.BottomMargin = cm(bottomMargin);
section.PageSetup.LeftMargin = cm(leftMargin);
section.PageSetup.RightMargin = cm(rightMargin);
section.PageSetup.DifferentFirstPageHeaderFooter = false;
return section;
}
public static Section CreatePage_Letter(Document doc, FdsTextBlocks tb,
bool draft = false, string tgtFont = "Arial")
{
if (!SystemFontExists(tgtFont)) tgtFont = "Arial";
DefineStyles_Standard(doc, tgtFont);
DefineStyles_Letter(doc, Array.Empty<Style>(), tgtFont);
var section = doc.AddSection();
section.PageSetup.TopMargin = cm(1.8);
section.PageSetup.BottomMargin = cm(1.8);
section.PageSetup.LeftMargin = cm(2.5);
section.PageSetup.RightMargin = cm(2.0);
section.PageSetup.DifferentFirstPageHeaderFooter = true;
string dataBase = Path.Combine(AppContext.BaseDirectory, "Data");
// ── Header logos ──────────────────────────────────────────────────────
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image1.png"),
width: mm(155.5), top: cm(0.79), left: cm(2.0));
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image2.png"),
width: mm(34.5), top: cm(0.59), left: cm(15.27));
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image3.png"),
width: mm(25.4), top: cm(6.21), left: cm(17.51));
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image4.png"),
width: mm(25.4), top: cm(7.79), left: cm(17.5));
if (draft)
{
void AddOverlay(HeaderFooter hf)
=> AddHeaderImage(hf, Path.Combine(dataBase, "overlay.png"),
width: cm(12), top: ShapePosition.Center, left: ShapePosition.Center);
AddOverlay(section.Headers.FirstPage);
AddOverlay(section.Headers.Primary);
}
// ── Fold / punch marks ────────────────────────────────────────────────
using var markBmp = new System.Drawing.Bitmap(20, 1);
for (int x = 0; x < 20; x++) markBmp.SetPixel(x, 0, System.Drawing.Color.Black);
string markB64 = MigraDocFilenameFromByteArray(ImageToByteArray(markBmp));
void AddMark(HeaderFooter hf, float top)
{
var img = hf.AddImage(markB64);
img.LockAspectRatio = true;
img.Top = cm(top); img.Left = 0;
img.RelativeVertical = RelativeVertical.Page;
img.RelativeHorizontal = RelativeHorizontal.Page;
img.WrapFormat.Style = WrapStyle.Through;
}
AddMark(section.Headers.FirstPage, 10.7f);
AddMark(section.Headers.Primary, 10.7f);
AddMark(section.Headers.FirstPage, 14.85f);
AddMark(section.Headers.Primary, 14.85f);
// ── Sender pre-print box ──────────────────────────────────────────────
{
var tf = section.Headers.FirstPage.AddTextFrame();
tf.RelativeVertical = RelativeVertical.Page;
tf.RelativeHorizontal = RelativeHorizontal.Page;
tf.Top = cm(4.65); tf.Left = cm(2.0);
tf.Width = cm(8.5); tf.Height = cm(0.6);
var p = tf.AddParagraph();
p.Style = "AddressBoxSender";
p.AddText($"{tb.SenderLine1} \u25cf {tb.SenderLine2}");
}
// ── Recipient address box ─────────────────────────────────────────────
{
var tf = section.Headers.FirstPage.AddTextFrame();
tf.RelativeVertical = RelativeVertical.Page;
tf.RelativeHorizontal = RelativeHorizontal.Page;
tf.Top = cm(5.6); tf.Left = cm(2.0);
tf.Width = cm(9); tf.Height = cm(4);
tf.MarginTop = cm(0.2);
if (tb.Address.Length > 0)
{
var p = tf.AddParagraph();
p.Style = "AddressBox";
foreach (string t in tb.Address) { p.AddText(t); p.AddLineBreak(); }
}
}
// ── Admin info block (right side) ─────────────────────────────────────
{
var tf = section.Headers.FirstPage.AddTextFrame();
tf.RelativeVertical = RelativeVertical.Page;
tf.RelativeHorizontal = RelativeHorizontal.Page;
tf.Top = cm(5.6); tf.Left = cm(13.0);
tf.Width = cm(5.5); tf.Height = cm(5.5);
void Row(string label, string value)
{
var p = tf.AddParagraph(); p.Style = "AdminInfo";
p.AddFormattedText(label + ": ", TextFormat.Bold);
p.AddText(value);
}
Row(tb.AdminDatumLabel, tb.AdminDatum);
if (!string.IsNullOrEmpty(tb.AdminRef))
Row("Nummer", tb.AdminRef);
if (!string.IsNullOrEmpty(tb.ProvisionPeriod))
Row(tb.AdminProvLabel, tb.ProvisionPeriod);
Row("Sachbearbeiter", tb.AdminUser);
Row("E-Mail", tb.AdminUserEmail);
}
// ── Ort und Zeit ──────────────────────────────────────────────────────
if (!string.IsNullOrEmpty(tb.OrtUndZeit))
{
var tf = section.Headers.FirstPage.AddTextFrame();
tf.Top = cm(11); tf.Left = ShapePosition.Right;
tf.Height = cm(1); tf.Width = cm(7);
tf.RelativeVertical = RelativeVertical.Page;
tf.RelativeHorizontal = RelativeHorizontal.Margin;
var p = tf.AddParagraph();
p.AddText(tb.OrtUndZeit);
p.Format.Font.Name = tgtFont;
p.Format.Font.Size = 11;
p.Format.Alignment = ParagraphAlignment.Right;
}
// ── Footer (all pages) ────────────────────────────────────────────────
AddLetterFooter(section.Footers.Primary, tb, tgtFont);
AddLetterFooter(section.Footers.FirstPage, tb, tgtFont);
AddLetterFooter(section.Footers.EvenPage, tb, tgtFont);
// ── Subject + body ────────────────────────────────────────────────────
if (!string.IsNullOrEmpty(tb.Subject))
{
var p = section.AddParagraph();
p.Style = "BodyText";
p.Format.SpaceBefore = cm(10.5);
p.AddFormattedText(tb.Subject, TextFormat.Bold);
p.AddLineBreak(); p.AddLineBreak();
}
if (!string.IsNullOrEmpty(tb.Body))
{
var p = section.AddParagraph();
p.Style = "BodyText";
p.Format.SpaceBefore = pt(16);
p.Format.LineSpacingRule = LineSpacingRule.Multiple;
p.Format.LineSpacing = 1.25;
p.AddText(tb.Body);
}
return section;
}
// ── Style definitions ─────────────────────────────────────────────────────
public static void DefineStyles_Standard(Document doc, string tgtFont = "Arial")
{
if (!SystemFontExists(tgtFont)) tgtFont = "Arial";
var normal = doc.Styles["Normal"]!;
normal.Font.Name = tgtFont;
normal.Font.Size = 11;
AddStyle(doc, "PageNumStyle", "Normal", s => s.Font.Size = 9);
AddStyle(doc, "BodyText", "Normal", s => { s.Font.Size = 11; s.ParagraphFormat.LineSpacing = 1.15; s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Multiple; });
AddStyle(doc, "AddressBox", "Normal", s => { s.Font.Size = 10; s.Font.Name = tgtFont; });
AddStyle(doc, "AddressBoxSender", "Normal", s => { s.Font.Size = 7; s.Font.Name = tgtFont; s.Font.Color = FuchsGray; });
AddStyle(doc, "AdminInfo", "Normal", s => s.Font.Size = 9);
AddStyle(doc, "FooterText", "Normal", s => { s.Font.Size = 8; s.Font.Color = FuchsGray; });
// List styles
AddStyle(doc, "ListStart", "Normal", s => { s.ParagraphFormat.SpaceBefore = 6; });
AddStyle(doc, "ListEnd", "Normal", s => { s.ParagraphFormat.SpaceAfter = 6; });
AddStyle(doc, "UnorderedList", "Normal", s =>
{
var li = s.ParagraphFormat.ListInfo;
li.ContinuePreviousList = true;
li.ListType = ListType.BulletList1;
});
AddStyle(doc, "OrderedList", "Normal", s =>
{
var li = s.ParagraphFormat.ListInfo;
li.ContinuePreviousList = true;
li.ListType = ListType.NumberList1;
});
}
public static void DefineStyles_Letter(Document doc, Style[] addStyles, string tgtFont = "Arial", float baseSize = 12f)
{
if (!SystemFontExists(tgtFont)) tgtFont = "Arial";
AddStyle(doc, "SubjectBig", "Normal", s => { s.Font.Name = tgtFont; s.Font.Size = 13; s.Font.Bold = true; });
AddStyle(doc, "HorizontalRule", "Normal", s =>
{
s.ParagraphFormat.Borders.Bottom.Width = 0.75;
s.ParagraphFormat.Borders.Bottom.Color = Colors.Black;
s.ParagraphFormat.SpaceAfter = 3;
});
foreach (var st in addStyles) doc.Styles.Add(st);
}
public static void Apply_Invoice_Styles(Document doc, string tgtFont = "Arial")
{
if (!SystemFontExists(tgtFont)) tgtFont = "Arial";
ParagraphFormat ClonePf()
{
var pf = doc.Styles["Normal"]!.ParagraphFormat.Clone();
pf.Borders.Distance = 3;
pf.Alignment = ParagraphAlignment.Left;
return pf;
}
AddStyle(doc, "SubjectBig", "Normal", s => { s.Font = new Font(tgtFont, 13); s.Font.Bold = true; });
AddStyle(doc, "AdminInfo", "Normal", s => s.Font = new Font(tgtFont, 9));
AddStyle(doc, "ProvisionLocation", "Normal", s => s.Font = new Font(tgtFont, 9));
AddStyle(doc, "InvoiceBody", "Normal", s => s.Font = new Font(tgtFont, 9));
var cellBase = AddStyle(doc, "TblCell_Base", "Normal", s => { s.Font = new Font(tgtFont, 9); s.ParagraphFormat = ClonePf(); });
AddStyle(doc, "TblCell_Head", "TblCell_Base", s => s.Font = new Font(tgtFont, 9));
var ttPf = ClonePf();
ttPf.LineSpacingRule = LineSpacingRule.Multiple; ttPf.LineSpacing = 1.15;
ttPf.Borders.DistanceFromTop = 9; ttPf.Borders.DistanceFromBottom = 6;
AddStyle(doc, "TblCell_RTitle", "TblCell_Base", s => { s.Font = new Font(tgtFont, 9) { Bold = true }; s.ParagraphFormat = ttPf; });
AddStyle(doc, "TblCell_RSum", "TblCell_Base", s => s.Font = new Font(tgtFont, 9) { Bold = true });
AddStyle(doc, "TblCell_TNet", "TblCell_Base", s => s.Font = new Font(tgtFont, 9) { Bold = true });
AddStyle(doc, "TblCell_TVat", "TblCell_Base", s => s.Font = new Font(tgtFont, 9));
AddStyle(doc, "TblCell_TSum", "TblCell_Base", s => s.Font = new Font(tgtFont, 9) { Bold = true });
var inPf = doc.Styles["Normal"]!.ParagraphFormat.Clone();
inPf.LineSpacingRule = LineSpacingRule.Multiple; inPf.LineSpacing = 1.05;
inPf.SpaceBefore = 10; inPf.Alignment = ParagraphAlignment.Justify;
AddStyle(doc, "InvoiceNotes", "Normal", s => { s.Font = new Font(tgtFont, 9); s.ParagraphFormat = inPf; });
AddStyle(doc, "InvoiceNotes_ucb", "InvoiceNotes", s => s.Font = new Font(tgtFont, 9) { Bold = true });
}
// ── ApplyInvoice ──────────────────────────────────────────────────────────
public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false)
{
string invoiceType = inv.InvoiceType;
bool p13b = inv.InvoiceRegistration?.getString("InvoiceOptions")
.Split(',').Contains("§13b") ?? false;
Apply_Invoice_Styles(doc);
var sec = doc.Sections.Cast<Section>().First();
sec.PageSetup.RightMargin = cm(1.5);
// Title
{
var p = sec.AddParagraph();
p.Style = "SubjectBig";
p.Format.SpaceBefore = cm(8.65);
p.AddText($"{inv.InvoiceTitle} Nr. {inv.InvoiceId}");
p.Format.SpaceAfter = cm(0.5);
}
// Provision location
if (inv.ProvisionLocation.Length > 0)
{
var p = sec.AddParagraph();
p.Style = "AddressBox";
p.Format.Font.Size = 10;
p.Format.LineSpacingRule = LineSpacingRule.Exactly;
p.Format.LineSpacing = 12;
p.AddFormattedText(tb.ProvisionLoc_Label, TextFormat.Bold);
p.AddLineBreak();
foreach (string t in inv.ProvisionLocation) { p.AddText(t); p.AddLineBreak(); }
}
sec.AddParagraph().Format.SpaceBefore = cm(0.7);
// Invoice items table
var tbl = sec.AddTable();
tbl.Style = "Table";
tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1);
tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25;
tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5;
tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6);
tbl.Rows.HeightRule = RowHeightRule.AtLeast;
double pageW = sec.PageSetup.PageWidth.Centimeter
- sec.PageSetup.LeftMargin.Centimeter
- sec.PageSetup.RightMargin.Centimeter;
double[] colWidths = { pageW * 0.06, pageW * 0.42, pageW * 0.12, pageW * 0.18, pageW * 0.22 };
foreach (double w in colWidths) tbl.AddColumn(cm(w));
// Header row
string[] headers = { "Pos.", "Bezeichnung", "Menge", "Einzelpreis", "Gesamtpreis" };
var hRow = tbl.AddRow();
hRow.Shading = GrayShading();
for (int i = 0; i < headers.Length; i++)
{
var p = hRow.Cells[i].AddParagraph(); p.Style = "TblCell_Head";
p.AddFormattedText(headers[i], TextFormat.Bold);
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
}
// Data rows — from InvoiceItems
int pos = 1;
foreach (var itm in inv.InvoiceItems)
{
string title = itm.nz("title", "");
string desc = itm.nz("desc", "");
string qty = itm.nz("qty", "1");
ParseDec(itm.no("price_net", 0), out decimal priceNet);
ParseDec(itm.no("total_net", 0), out decimal totalNet);
var row = tbl.AddRow();
row.HeightRule = RowHeightRule.Auto;
row.Cells[0].AddParagraph(pos.ToString()).Style = "TblCell_Base";
var titleCell = row.Cells[1].AddParagraph();
titleCell.Style = "TblCell_RTitle"; titleCell.AddText(title);
if (!string.IsNullOrEmpty(desc)) row.Cells[1].AddHtml($"<div>{desc}</div>");
row.Cells[2].AddParagraph(qty).Style = "TblCell_Base";
row.Cells[3].AddParagraph(Currency(priceNet)).Style = "TblCell_Base";
row.Cells[4].AddParagraph(Currency(totalNet)).Style = "TblCell_RSum";
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
row.Cells[4].Format.Alignment = ParagraphAlignment.Right;
pos++;
}
// Totals
void TotalRow(string label, string value, string style = "TblCell_TSum", bool topBorder = false)
{
var r = tbl.AddRow(); r.Shading = LightShading();
if (topBorder) r.Borders.Top.Width = 0.5;
var lp = r.Cells[3].AddParagraph(); lp.Style = style; lp.AddText(label);
r.Cells[3].Format.Alignment = ParagraphAlignment.Left;
var vp = r.Cells[4].AddParagraph(); vp.Style = style; vp.AddText(value);
r.Cells[4].Format.Alignment = ParagraphAlignment.Right;
}
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance_net"), out decimal netSum);
TotalRow("Nettobetrag:", Currency(netSum), "TblCell_TNet", topBorder: true);
// VAT rows
foreach (var vr in inv.VatRows)
{
string pct = vr.Key;
ParseDec(vr.Value.no("vat_amount", 0), out decimal vatAmt);
TotalRow($"MwSt. {pct}%:", Currency(vatAmt), "TblCell_TVat");
}
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal gross);
TotalRow("Rechnungsbetrag:", Currency(gross), "TblCell_TSum");
// Payment terms note
string terms = TranslatePaymentTerm(inv.PaymentTerms);
string ibanLine = p13b
? "Die Steuerschuldnerschaft geht auf den Leistungsempf\u00e4nger \u00fcber (§ 13b UStG)."
: $"Bitte \u00fcberweisen Sie den Rechnungsbetrag innerhalb von {terms} auf unser Konto:\n" +
"IBAN: DE76\u00a03005\u00a00110\u00a00045\u00a00148\u00a000, BIC DUSSSDEDDXXX (Stadtsparkasse D\u00fcsseldorf)";
{
var p = sec.AddParagraph(); p.Style = "InvoiceNotes";
p.AddText(ibanLine);
}
}
// ── ApplyReminder ─────────────────────────────────────────────────────────
public static void ApplyReminder(Document doc, FdsTextBlocks tb, FdsReminderData rem, bool draft = false)
{
var sec = doc.Sections.Cast<Section>().First();
string rtype = rem.ReminderType;
// Opening text
if (tb.ReminderTexts_before.TryGetValue(rtype, out var intro))
{
var p = sec.AddParagraph(); p.Style = "BodyText";
p.Format.SpaceBefore = cm(0.5);
foreach (string line in intro) { p.AddText(line); p.AddLineBreak(); }
}
// Reminder items table
var tbl = sec.AddTable();
tbl.Style = "Table";
tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25;
tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6);
tbl.Rows.HeightRule = RowHeightRule.AtLeast;
tbl.Format.SpaceBefore = mm(4);
double pageW = sec.PageSetup.PageWidth.Centimeter
- sec.PageSetup.LeftMargin.Centimeter
- sec.PageSetup.RightMargin.Centimeter;
tbl.AddColumn(cm(pageW * 0.15));
tbl.AddColumn(cm(pageW * 0.45));
tbl.AddColumn(cm(pageW * 0.20));
tbl.AddColumn(cm(pageW * 0.20));
string[] headers = { "Datum", "Bezeichnung", "Betrag", "Offen" };
var hRow = tbl.AddRow(); hRow.Shading = GrayShading();
for (int i = 0; i < headers.Length; i++)
{
var p = hRow.Cells[i].AddParagraph();
p.AddFormattedText(headers[i], TextFormat.Bold);
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
}
foreach (var itm in rem.ReminderItems)
{
var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto;
row.Cells[0].AddParagraph(itm.nz("InvoiceDate", ""));
row.Cells[1].AddParagraph(itm.nz("DocumentName", "").ne(itm.nz("InvoiceTitle", "")));
row.Cells[2].AddParagraph(Currency(itm.no("InvoiceBalance", 0)));
row.Cells[3].AddParagraph(Currency(itm.no("amount_open", 0)));
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
}
// Open total
ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal);
var sumRow = tbl.AddRow(); sumRow.Shading = LightShading();
sumRow.Borders.Top.Width = 0.5;
sumRow.Cells[2].AddParagraph().AddFormattedText("Offener Betrag:", TextFormat.Bold);
var sv = sumRow.Cells[3].AddParagraph();
sv.AddFormattedText(Currency(openTotal), TextFormat.Bold);
sumRow.Cells[3].Format.Alignment = ParagraphAlignment.Right;
// Closing text + QR code
var closing = sec.AddParagraph(); closing.Style = "BodyText";
closing.Format.SpaceBefore = cm(0.8);
string terms = TranslatePaymentTerm(rem.ReminderRegistration?.getString("PaymentTerm") ?? "");
closing.AddText(
$"Bitte \u00fcberweisen Sie den offenen Betrag von {Currency(openTotal)} innerhalb von {terms} auf unser Konto:\n" +
"IBAN: DE76\u00a03005\u00a00110\u00a00045\u00a00148\u00a000, BIC DUSSSDEDDXXX (Stadtsparkasse D\u00fcsseldorf)");
// Greeting
var greet = sec.AddParagraph(); greet.Style = "BodyText";
greet.Format.SpaceBefore = cm(0.8);
greet.AddText("Mit freundlichen Gr\u00fc\u00dfen\nSebastian Fuchs GmbH \u0026 Co. KG");
}
// ── WriteLetter (wrapper) ─────────────────────────────────────────────────
public static Task<Document> WriteLetter(FdsTextBlocks tb, bool draft, CultureInfo locale)
{
var doc = new Document();
doc.Info.Author = tb.DocInfo_Author;
doc.Info.Subject = $"by Processweb, Dr. Stefan Ott, \u00a9 {DateTime.Now.Year}";
DefineStyles_Standard(doc);
DefineStyles_Letter(doc, Array.Empty<Style>());
CreatePage_Letter(doc, tb, draft);
return Task.FromResult(doc);
}
// ── PDF rendering helpers ─────────────────────────────────────────────────
/// <summary>Renders a MigraDoc Document to a PDF/A byte array.</summary>
public static byte[] DocToPdfBytes(Document doc)
{
var renderer = new PdfDocumentRenderer() { Document = doc };
renderer.RenderDocument();
using var ms = new MemoryStream();
renderer.PdfDocument.Save(ms, closeStream: false);
ms.Position = 0;
return OCORE.pdf._pdf.pdfAFileContent(ms.ToArray());
}
/// <summary>Renders a MigraDoc Document to an ImageCollection for preview.</summary>
public static async Task<OCORE.pdf._pdf.ImageCollection> DocToImageCollection(Document doc)
{
byte[] bytes = DocToPdfBytes(doc);
using var ms = new MemoryStream(bytes);
using var spire = new Spire.Pdf.PdfDocument();
spire.LoadFromStream(ms);
return await OCORE.pdf._pdf.pdfImageresultAsync(spire);
}
/// <summary>Converts a PDF byte array to an ImageCollection.</summary>
public static async Task<OCORE.pdf._pdf.ImageCollection> BytesToImageCollection(byte[] pdfBytes)
{
using var ms = new MemoryStream(pdfBytes);
using var spire = new Spire.Pdf.PdfDocument();
spire.LoadFromStream(ms);
return await OCORE.pdf._pdf.pdfImageresultAsync(spire);
}
// ── Private helpers ───────────────────────────────────────────────────────
private static Style AddStyle(Document doc, string name, string basedOn, Action<Style> configure)
{
Style? s;
try { s = doc.Styles.AddStyle(name, basedOn); }
catch { s = doc.Styles[name]; } // already exists in multi-page docs
configure(s!);
return s!;
}
private static void AddHeaderImage(HeaderFooter hf, string path,
Unit width, Unit top, Unit left)
{
if (!File.Exists(path)) return;
var img = hf.AddImage(path);
img.Width = width; img.LockAspectRatio = true;
img.Top = top; img.Left = left;
img.RelativeVertical = RelativeVertical.Page;
img.RelativeHorizontal = RelativeHorizontal.Page;
img.WrapFormat.Style = WrapStyle.Through;
}
private static void AddHeaderImage(HeaderFooter hf, string path,
Unit width, ShapePosition top, ShapePosition left)
{
if (!File.Exists(path)) return;
var img = hf.AddImage(path);
img.Width = width; img.LockAspectRatio = true;
img.Top = top;
img.Left = left;
img.RelativeVertical = RelativeVertical.Page;
img.RelativeHorizontal = RelativeHorizontal.Page;
img.WrapFormat.Style = WrapStyle.Through;
}
private static void AddLetterFooter(HeaderFooter footer, FdsTextBlocks tb, string font)
{
// Horizontal rule
var rule = footer.AddParagraph(); rule.Style = "FooterText";
rule.Format.Borders.Top.Width = 0.5;
rule.Format.Borders.Top.Color = FuchsGray;
rule.Format.SpaceBefore = 4;
// Four-column footer table
var tbl = footer.AddTable();
tbl.Format.Font.Name = font; tbl.Format.Font.Size = 8;
tbl.Format.Font.Color = FuchsGray;
tbl.Borders.Visible = false;
double[] widths = { 4.2, 4.8, 4.2, 4.8 };
foreach (double w in widths) tbl.AddColumn(cm(w));
var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto;
string[][] blocks = { tb.FooterBlock1, tb.FooterBlock2, tb.FooterBlock3, tb.FooterBlock4 };
for (int col = 0; col < 4; col++)
{
var p = row.Cells[col].AddParagraph();
foreach (string line in blocks[col]) { p.AddText(line); p.AddLineBreak(); }
}
}
private static string MigraDocFilenameFromByteArray(byte[] image) =>
"base64:" + Convert.ToBase64String(image);
private static byte[] ImageToByteArray(System.Drawing.Image img)
{
using var ms = new MemoryStream();
img.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
return ms.ToArray();
}
private static bool SystemFontExists(string fontName)
{
try
{
using var f = new System.Drawing.Font(fontName, 8f);
return string.Equals(fontName, f.Name, StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
}
+20
View File
@@ -0,0 +1,20 @@
using Fuchs.Controllers;
using Microsoft.AspNetCore.Mvc;
using static OCORE.web.mvc_helper_async;
namespace Fuchs.intranet;
/// <summary>
/// Report processing helpers for the Fuchs intranet.
/// Delegates to the SQL-driven report catalog.
/// </summary>
public static class FuchsReports
{
public static async Task<IActionResult> ProcessFdsRequest(
IntranetController ctrl, string action, string id)
{
// Specific report actions are dispatched here.
// Extend with additional cases from the VB fuchs_reports.vb as needed.
return new OkResult();
}
}
+190
View File
@@ -0,0 +1,190 @@
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();
}
+206
View File
@@ -0,0 +1,206 @@
using HtmlAgilityPack;
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Shapes;
using MigraDoc.DocumentObjectModel.Tables;
using System.Net;
// Ported from migradoc_Extensions.vb + migradoc_HtmlConverter.vb
namespace MigraDoc.Extensions
{
/// <summary>Abstract base for HTML-to-MigraDoc converters.</summary>
public abstract class IConverter
{
public abstract Action<Section> Convert(string contents);
public abstract Action<TextFrame> ConvertTextFrame(string contents);
public abstract Action<Cell> ConvertCell(string contents);
}
/// <summary>Extension methods that add HTML content to MigraDoc objects.</summary>
public static class Extensions
{
// Paragraph.SetStyle() was removed in PDFsharp 6 — provide it as extension
public static Paragraph WithStyle(this Paragraph p, string style) { p.Style = style; return p; }
public static Section AddHtml(this Section section, string html)
=> section.Add(html, new MigraDoc.Extensions.Html.HtmlConverter());
public static Cell AddHtml(this Cell cell, string html)
=> cell.Add(html, new MigraDoc.Extensions.Html.HtmlConverter());
public static TextFrame AddHtml(this TextFrame frame, string html)
=> frame.Add(html, new MigraDoc.Extensions.Html.HtmlConverter());
internal static Section Add(this Section section, string contents, IConverter converter)
{
if (string.IsNullOrEmpty(contents)) throw new ArgumentNullException(nameof(contents));
if (converter is null) throw new ArgumentNullException(nameof(converter));
converter.Convert(contents)(section);
return section;
}
internal static Cell Add(this Cell cell, string contents, IConverter converter)
{
if (string.IsNullOrEmpty(contents)) throw new ArgumentNullException(nameof(contents));
if (converter is null) throw new ArgumentNullException(nameof(converter));
converter.ConvertCell(contents)(cell);
return cell;
}
internal static TextFrame Add(this TextFrame frame, string contents, IConverter converter)
{
if (string.IsNullOrEmpty(contents)) throw new ArgumentNullException(nameof(contents));
if (converter is null) throw new ArgumentNullException(nameof(converter));
converter.ConvertTextFrame(contents)(frame);
return frame;
}
}
}
namespace MigraDoc.Extensions.Html
{
/// <summary>Converts a subset of HTML to MigraDoc document objects.</summary>
public class HtmlConverter : MigraDoc.Extensions.IConverter
{
private readonly IDictionary<string, Func<HtmlNode, DocumentObject, DocumentObject>> _handlers;
public HtmlConverter()
{
_handlers = new Dictionary<string, Func<HtmlNode, DocumentObject, DocumentObject>>();
AddDefaultHandlers();
}
public IDictionary<string, Func<HtmlNode, DocumentObject, DocumentObject>> NodeHandlers => _handlers;
public override Action<Section> Convert(string c) => s => Run(c, s);
public override Action<TextFrame> ConvertTextFrame(string c) => f => Run(c, f);
public override Action<Cell> ConvertCell(string c) => cell => Run(c, cell);
private void Run(string html, DocumentObject root)
{
var doc = new HtmlDocument(); doc.LoadHtml(html);
Walk(doc.DocumentNode.ChildNodes, root);
}
private void Walk(HtmlNodeCollection nodes, DocumentObject parent)
{
foreach (var node in nodes)
{
if (_handlers.TryGetValue(node.Name, out var h))
{
var result = h(node, parent);
if (node.HasChildNodes) Walk(node.ChildNodes, result);
}
else if (node.HasChildNodes)
Walk(node.ChildNodes, parent);
}
}
private void AddDefaultHandlers()
{
// Block elements — return new Paragraph as child target
_handlers["p"] = _handlers["div"] = (n, p) => MkPara(p, "");
_handlers["br"] = (n, p) =>
{
if (p is FormattedText ft) { ft.AddLineBreak(); return p; }
var para = GetPara(p); para?.AddLineBreak(); return (DocumentObject?)para ?? p;
};
_handlers["hr"] = (n, p) => { var para = GetPara(p); if (para != null) para.Style = "HorizontalRule"; return (DocumentObject?)para ?? p; };
// Headings
foreach (int i in Enumerable.Range(1, 6))
{
int lvl = i;
_handlers[$"h{lvl}"] = (n, p) =>
{
string style = "Heading" + lvl;
if (p is Cell c) return c.AddParagraph().WithStyle(style);
return ((Section)p).AddParagraph().WithStyle(style);
};
}
// Inline formatting
_handlers["b"] = _handlers["strong"] = (n, p) => Fmt(p, TextFormat.Bold);
_handlers["i"] = _handlers["em"] = (n, p) => Fmt(p, TextFormat.Italic);
_handlers["u"] = (n, p) => Fmt(p, TextFormat.Underline);
// Hyperlink
_handlers["a"] = (n, p) =>
GetPara(p)?.AddHyperlink(n.GetAttributeValue("href", ""), HyperlinkType.Web)
as DocumentObject ?? p;
// List items
_handlers["li"] = (n, p) =>
{
string listStyle = n.ParentNode?.Name == "ul" ? "UnorderedList" : "OrderedList";
var siblings = n.ParentNode?.Elements("li")?.ToList() ?? new List<HtmlNode>();
bool first = siblings.FirstOrDefault() == n;
bool last = siblings.LastOrDefault() == n;
return AddListItem(p, listStyle, first, last);
};
// Text node
_handlers["#text"] = (n, p) =>
{
string text = WebUtility.HtmlDecode(
n.InnerText.Replace("\r", "").Replace("\n", ""));
if (string.IsNullOrWhiteSpace(text)) return p;
if (p is FormattedText ft) return ft.AddText(text);
if (p is Hyperlink hl) return hl.AddText(text);
return GetPara(p)?.AddText(text) as DocumentObject ?? p;
};
}
// ── Helpers ───────────────────────────────────────────────────────────
private static DocumentObject Fmt(DocumentObject parent, TextFormat fmt)
{
if (parent is FormattedText ft) return ft.AddFormattedText(fmt);
return GetPara(parent)?.AddFormattedText(fmt) as DocumentObject ?? parent;
}
private static DocumentObject MkPara(DocumentObject parent, string style)
{
Paragraph? p = parent switch
{
Cell c => c.AddParagraph(),
Section s => s.AddParagraph(),
_ => GetPara(parent)
};
if (p != null && !string.IsNullOrEmpty(style)) p.Style = style;
return (DocumentObject?)p ?? parent;
}
private static DocumentObject AddListItem(DocumentObject parent,
string listStyle, bool first, bool last)
{
Paragraph? item = null;
if (parent is Cell cell)
{
if (first) cell.AddParagraph().Style = "ListStart";
item = cell.AddParagraph(); item.Style = listStyle;
item.Format.ListInfo.ContinuePreviousList = !first;
if (last) cell.AddParagraph().Style = "ListEnd";
}
else if (parent is TextFrame frame)
{
if (first) frame.AddParagraph().Style = "ListStart";
item = frame.AddParagraph(); item.Style = listStyle;
item.Format.ListInfo.ContinuePreviousList = !first;
if (last) frame.AddParagraph().Style = "ListEnd";
}
else if (parent is Section sec)
{
if (first) sec.AddParagraph().Style = "ListStart";
item = sec.AddParagraph(); item.Style = listStyle;
item.Format.ListInfo.ContinuePreviousList = !first;
if (last) sec.AddParagraph().Style = "ListEnd";
}
return (DocumentObject?)item ?? parent;
}
internal static Paragraph? GetPara(DocumentObject parent) => parent switch
{
Cell c => c.AddParagraph(),
Paragraph p => p,
Section s => s.AddParagraph(),
TextFrame f => f.AddParagraph(),
_ => null
};
}
}