Refactor code structure and remove redundant sections for improved readability and maintainability

This commit is contained in:
Stefan
2026-07-10 21:03:55 +02:00
parent 42997c4f49
commit 83d1c28b29
18 changed files with 696 additions and 86 deletions
+31 -2
View File
@@ -19,7 +19,7 @@ public class FdsInvoiceData
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 List<Dictionary<string, object>>? Req { get; internal set; }
public GenericObjectDictionary? InvoiceRegistration { get; internal set; }
public bool IsDraft { get; internal set; } = true;
@@ -46,9 +46,25 @@ public class FdsInvoiceData
get
{
var result = new List<Dictionary<string, object?>>();
foreach (var block in InvoiceBlocks) result.AddRange(block.Items);
return result;
}
}
/// <summary>
/// The service-request groups as they should render on the invoice: each block carries its
/// heading (the section title the editor shows) and its line items. The PDF renders a heading
/// row per block followed by that block's items, so the online editor and the PDF stay in sync.
/// </summary>
public List<InvoiceBlock> InvoiceBlocks
{
get
{
var result = new List<InvoiceBlock>();
if (Req == null) return result;
foreach (var req in Req)
{
var items = new List<Dictionary<string, object?>>();
if (req.TryGetValue("items", out var itmsObj))
{
IEnumerable<Dictionary<string, object?>>? itms =
@@ -56,8 +72,12 @@ public class FdsInvoiceData
?? (itmsObj is JArray ja
? ja.ToObject<List<Dictionary<string, object?>>>()
: null);
if (itms != null) result.AddRange(itms);
if (itms != null) items.AddRange(itms);
}
string heading = "";
if (req.TryGetValue("text", out var th) && th != null) heading = th.ToString() ?? "";
if (heading.Length == 0 && req.TryGetValue("nme", out var nh) && nh != null) heading = nh.ToString() ?? "";
result.Add(new InvoiceBlock { Heading = heading, Items = items });
}
return result;
}
@@ -196,3 +216,12 @@ public class FdsInvoiceData
return double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0;
}
}
/// <summary>A service-request group as it renders on the invoice: a heading plus its line items.</summary>
public sealed class InvoiceBlock
{
/// <summary>The section heading (editor's <c>text</c>/<c>nme</c>); empty when the group has none.</summary>
public string Heading { get; init; } = "";
/// <summary>The group's line items (the editor's <c>items</c> contract).</summary>
public List<Dictionary<string, object?>> Items { get; init; } = new();
}
+65 -22
View File
@@ -137,6 +137,30 @@ public static class FuchsPdf
public static string TranslatePaymentTerm(string pt) =>
pt.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen");
/// <summary>
/// Maps one editor item-contract entry to a display line, mirroring the online editor: the
/// item's own price/total is shown, a set header is emphasised, and free-text/heading lines
/// (type <c>text</c>/<c>title</c>) show neither a price nor a position number. This is the flat
/// (non-collapsing) rendering used for every set mode except the explicit <c>setonly</c>.
/// </summary>
private static InvoiceSetLine MapItemToLine(Dictionary<string, object?> i)
{
string type = i.nz("type", "").ToLowerInvariant();
bool isText = type is "text" or "title";
ParseDec(i.no("price_net", 0), out decimal price);
ParseDec(i.no("total_net", 0), out decimal total);
return new InvoiceSetLine
{
Title = i.nz("title", ""),
Desc = i.nz("desc", ""),
Qty = i.nz("qty", ""),
PriceNet = price,
TotalNet = total,
ShowPrice = !isText,
IsSetHeader = type == "set"
};
}
/// <summary>
/// Parses a numeric value coming from JSON deserialization (long/double), SQL
/// (decimal), or an already-invariant numeric string. Numeric CLR types are
@@ -593,30 +617,49 @@ public static class FuchsPdf
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
}
// Data rows — resolved through the set-display mode (see InvoiceSetPricing).
// For invoices without sets this passes items through unchanged; for sets it
// emits set header + members per the chosen mode, blanking price cells where
// a line should show no price. Totals come from the registration balance, so
// the mode is purely presentational.
// Data rows — grouped by service-request block (see FdsInvoiceData.InvoiceBlocks).
// Per the product decision the PDF must mirror the online editor exactly: each section
// prints its heading, every position shows its own price, and positions are numbered the
// same way the editor numbers them (every line except free-text/heading lines, including a
// set header). Set-display collapsing is honoured only for the explicit SetOnly mode; every
// other mode renders the items flat, so nothing is silently blanked or renumbered.
var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions"));
int pos = 1;
foreach (var line in InvoiceSetPricing.Build(inv.InvoiceItems, setMode))
int pos = 0;
foreach (var block in inv.InvoiceBlocks)
{
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";
if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold);
else titleCell.AddText(line.Title);
if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"<div>{line.Desc}</div>");
row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base";
row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base";
row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.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++;
if (!string.IsNullOrWhiteSpace(block.Heading))
{
var hr = tbl.AddRow();
hr.HeightRule = RowHeightRule.Auto;
hr.Cells[1].MergeRight = 3; // span Bezeichnung … Gesamtpreis
hr.Cells[1].AddParagraph().WithStyle("TblCell_RTitle").AddFormattedText(block.Heading, TextFormat.Bold);
}
var lines = setMode == SetDisplayMode.SetOnly && InvoiceSetPricing.ContainsSets(block.Items)
? InvoiceSetPricing.Build(block.Items, SetDisplayMode.SetOnly) // only this mode collapses members
: block.Items.Select(MapItemToLine).ToList(); // flat: faithful mirror of the editor
foreach (var line in lines)
{
bool numbered = line.IsSetHeader || line.ShowPrice; // free-text/heading lines carry no number
var row = tbl.AddRow();
row.HeightRule = RowHeightRule.Auto;
row.Cells[0].AddParagraph(numbered ? (++pos).ToString() : "").Style = "TblCell_Base";
if (!string.IsNullOrEmpty(line.Title)) // skip the empty paragraph that added a blank line before free text
{
var titleCell = row.Cells[1].AddParagraph();
titleCell.Style = "TblCell_RTitle";
if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold);
else titleCell.AddText(line.Title);
}
if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"<div>{line.Desc}</div>");
row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base";
row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base";
row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.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;
}
}
// Totals
+25
View File
@@ -68,6 +68,31 @@ public static class InvoiceDraftCalculator
session.Sums = sums;
}
/// <summary>
/// Renumbers the visible line positions authoritatively (the port of the client-side
/// numbering in <c>invSumUpdate</c>): priced lines are numbered sequentially across the whole
/// invoice — matching the PDF's <c>Pos.</c> column — while heading/free-text lines
/// (<c>typ</c> = "text"/"title") carry no number. The result is written onto each line's
/// <c>p</c> field so it flows back to the browser (via the view state) and into the PDF; this
/// keeps the online editor and the PDF preview showing the same position numbers, including
/// after a reorder.
/// </summary>
public static void RecomputePositions(InvoiceDraftSession session)
{
int pos = 0;
foreach (var blockTok in session.Req)
{
if (blockTok is not JObject block || block["itm"] is not JArray lines) continue;
foreach (var lineTok in lines)
{
if (lineTok is not JObject co) continue;
string typ = Str(co["typ"]).Trim().ToLowerInvariant();
bool numbered = typ is not ("text" or "title"); // only headings/free-text carry no number (mirrors invSumUpdate)
co["p"] = numbered ? (JToken)(++pos) : (JToken)"";
}
}
}
/// <summary>
/// Refreshes the draft's plausibility / consistency findings. "error" severity marks
/// issues that should block a clean finalise; "warning" is advisory. Kept in German,