Add user-defined invoice set structure dialog (ADR 0016)
Playwright Tests / test (push) Canceled after 0s

Lets a back-office user correct/define which invoice line belongs to
which "set" and how deep it is nested, instead of relying only on the
implicit "everything after a Set header until the next Set" grouping
fds__prepInvoice derives. Sets may contain sets; every change applies
immediately to the cached draft session and is reflected in the editor
and PDF (ADR 0006/0008).

Backend:
- InvoiceSetStructure: pure parent-link forest (assign, change level,
  create, place/move-sibling, depth-first linearise + level recompute).
- InvoiceDraftSetStructureDeltas: new inv/dpatch targets set.assign,
  set.level, set.create, set.move.
- InvoiceSetPricing.Build is now order-preserving and nesting-aware
  (governed by the nearest converted ancestor); the set-item switch
  sums a header's transitive descendants so nested conversions never
  double-count.
- FuchsPdf indents nested set members per level, mirroring the editor.

Frontend:
- New fis.inv_sets.js: "Set-Struktur bearbeiten" dialog — per-block
  table of all lines with drag & drop reorder (before/after/into),
  ▲▼ order and ◀▶ level buttons, fold/unfold per set (view-only), and
  a create bar for new Set/Text lines. Posts one delta per action; the
  dialog is a pure view of the server state (ADR 0006).
- _Layout.cshtml / fis_main.js: cache-bust the dynamically imported
  per-module bundles (web/fis.<mdl>.<locale>.js/.css) with an asset
  version derived from the newest bundle write time, so a gulp rebuild
  is picked up without a stale cached module.

Docs: ADR 0016, live-draft-editing concept, INVOICE_SET_PRICING.md,
USER_GUIDE.md, ARCHITECTURE.md updated. Tests: InvoiceSetStructureTests
(new) + additions to InvoiceDraftServiceTests/InvoiceSetPricingTests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 08:55:05 +02:00
co-authored by Claude Sonnet 5
parent c1936a327e
commit ecc74cf3c6
31 changed files with 3003 additions and 147 deletions
+189
View File
@@ -1,3 +1,4 @@
@charset "UTF-8";
/* basics */
/* Color SCHEME */
/* #8396bd */
@@ -169,6 +170,33 @@ table.if td.num {
.edit_frm .invoice_layout .emailfrm:hover div.axf {
display: block;
}
.edit_frm .invoice_layout .emailccfrm {
position: absolute;
font-size: 0.7rem;
top: 2.2rem;
left: 0.5rem;
width: auto;
height: auto;
cursor: pointer;
}
.edit_frm .invoice_layout .emailccfrm .content {
color: #000;
}
.edit_frm .invoice_layout .emailccfrm {
/* Unlike the other optional fields (plain "-" placeholder), this one names the action —
there is no default "Zusatzempfänger" from MFR, so an empty state must read as an
offer to add one, not as missing data. */
}
.edit_frm .invoice_layout .emailccfrm .content:empty::before {
content: "Zusatzempfänger hinzufügen";
min-width: 5rem;
height: 1rem;
display: block;
position: absolute;
}
.edit_frm .invoice_layout .emailccfrm:hover div.axf {
display: block;
}
.edit_frm .invoice_layout .admfrm {
position: absolute;
right: 3rem;
@@ -302,6 +330,27 @@ table.if td.num {
.edit_frm .invoice_layout table.invi > tbody > tr.sethdr td.txt {
font-weight: bold;
}
.edit_frm .invoice_layout table.invi > tbody > tr {
/* Nested set members (ADR 0016): indent per level, mirroring the PDF's per-level indent. */
}
.edit_frm .invoice_layout table.invi > tbody > tr.lvl1 td.txt {
padding-left: calc(0.4rem + 1.4rem);
}
.edit_frm .invoice_layout table.invi > tbody > tr.lvl2 td.txt {
padding-left: calc(0.4rem + 2.8rem);
}
.edit_frm .invoice_layout table.invi > tbody > tr.lvl3 td.txt {
padding-left: calc(0.4rem + 4.2rem);
}
.edit_frm .invoice_layout table.invi > tbody > tr.lvl4 td.txt {
padding-left: calc(0.4rem + 5.6rem);
}
.edit_frm .invoice_layout table.invi > tbody > tr.lvl5 td.txt {
padding-left: calc(0.4rem + 7rem);
}
.edit_frm .invoice_layout table.invi > tbody > tr.lvl6 td.txt {
padding-left: calc(0.4rem + 8.4rem);
}
.edit_frm .invoice_layout table.invi th.currency, .edit_frm .invoice_layout table.invi th.num, .edit_frm .invoice_layout table.invi td.currency, .edit_frm .invoice_layout table.invi td.num {
text-align: right;
white-space: nowrap;
@@ -510,4 +559,144 @@ table.if td.num {
}
.adr-conformity .tx {
color: #445;
}
/* ── Set structure dialog (ADR 0016, see $inv.sets in fis.inv_sets.js) ─────────────────── */
.modal .setstruct.busy {
opacity: 0.6;
pointer-events: none;
}
.modal .setstruct div.shint {
font-size: 0.85rem;
color: #666;
margin: 0 0 0.8rem 0;
}
.modal .setstruct div.sblock {
margin: 0 0 1.4rem 0;
}
.modal .setstruct div.sblock div.sbhd {
font-weight: bold;
padding: 0.4rem 0.6rem;
border-top: 1.5px solid #000;
border-bottom: 1px solid #ddd;
}
.modal .setstruct div.sblock div.sbhd.dropok {
background-color: #e6f2e6;
outline: 2px dashed #5a5;
}
.modal .setstruct div.sblock table.stbl {
width: 100%;
border-collapse: collapse;
}
.modal .setstruct div.sblock table.stbl th {
text-align: left;
font-size: 0.8rem;
color: #666;
padding: 0.3rem 0.4rem;
border-bottom: 1px solid #ddd;
}
.modal .setstruct div.sblock table.stbl td {
padding: 0.3rem 0.4rem;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.modal .setstruct div.sblock table.stbl td.grip {
width: 1.6rem;
cursor: grab;
color: #999;
}
.modal .setstruct div.sblock table.stbl td.pos, .modal .setstruct div.sblock table.stbl td.qty {
width: 4rem;
white-space: nowrap;
}
.modal .setstruct div.sblock table.stbl td.typ {
width: 7rem;
white-space: nowrap;
color: #666;
font-size: 0.85rem;
}
.modal .setstruct div.sblock table.stbl td.typ span.fold {
cursor: pointer;
margin-right: 0.35rem;
color: #555;
display: inline-block;
width: 1rem;
}
.modal .setstruct div.sblock table.stbl td.ttl span.ind {
display: inline-block;
}
.modal .setstruct div.sblock table.stbl td.ttl span.seti {
margin-right: 0.3rem;
color: #888;
}
.modal .setstruct div.sblock table.stbl td.ttl span.cnt {
color: #999;
font-weight: normal;
font-size: 0.85rem;
}
.modal .setstruct div.sblock table.stbl td.act {
width: 7.5rem;
white-space: nowrap;
text-align: right;
}
.modal .setstruct div.sblock table.stbl td.act div.ibtn {
display: inline-block;
cursor: pointer;
padding: 0 0.25rem;
}
.modal .setstruct div.sblock table.stbl td.act div.ibtn.disabled {
opacity: 0.25;
cursor: default;
}
.modal .setstruct div.sblock table.stbl tr.srow.isset td.ttl {
font-weight: bold;
}
.modal .setstruct div.sblock table.stbl tr.srow.hid {
display: none;
}
.modal .setstruct div.sblock table.stbl tr.srow.folded td.ttl {
font-style: italic;
}
.modal .setstruct div.sblock table.stbl tr.srow.dragging {
opacity: 0.4;
}
.modal .setstruct div.sblock table.stbl tr.srow.dropok td {
background-color: #e6f2e6;
}
.modal .setstruct div.sblock table.stbl tr.srow.dropbefore td {
border-top: 3px solid #5a5;
}
.modal .setstruct div.sblock table.stbl tr.srow.dropafter td {
border-bottom: 3px solid #5a5;
}
.modal .setstruct div.sblock div.sroot {
margin: 0.3rem 0 0.5rem 0;
padding: 0.4rem 0.6rem;
border: 1px dashed #bbb;
color: #888;
font-size: 0.85rem;
}
.modal .setstruct div.sblock div.sroot.dropok {
background-color: #e6f2e6;
border-color: #5a5;
color: #363;
}
.modal .setstruct div.sblock div.screate {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.6rem;
background-color: #f7f7f7;
}
.modal .setstruct div.sblock div.screate span.lbl {
font-size: 0.85rem;
color: #666;
}
.modal .setstruct div.sblock div.screate select, .modal .setstruct div.sblock div.screate input[type=text] {
padding: 0.2rem 0.3rem;
}
.modal .setstruct div.sblock div.screate input[type=text] {
flex: 1 1 14rem;
min-width: 10rem;
}