Initial Commit after switching from SVN to git
This commit is contained in:
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> </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; }
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user