Add user-defined invoice set structure dialog (ADR 0016)
Playwright Tests / test (push) Canceled after 0s
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:
@@ -1,3 +1,4 @@
|
||||
@charset "UTF-8";
|
||||
/* basics */
|
||||
/* Color SCHEME */
|
||||
/* #8396bd */
|
||||
@@ -306,6 +307,33 @@ table.if th.keep, table.if td.keep {
|
||||
.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;
|
||||
@@ -439,6 +467,27 @@ table.if th.keep, table.if td.keep {
|
||||
.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;
|
||||
@@ -647,4 +696,144 @@ table.if th.keep, table.if td.keep {
|
||||
}
|
||||
.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;
|
||||
}
|
||||
Reference in New Issue
Block a user