From c182d4fd69483025e33343eed3a9b6d018628de5 Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 10 Sep 2026 19:38:03 +0200 Subject: [PATCH] Add row context menus, fix glyphicon copy corruption, combine CC email editing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fuchs/gulpfile.js: pass encoding:false to gulp.src() in the copy task — vinyl-fs otherwise UTF-8-decodes binary files by default, which was silently corrupting the glyphicons webfont (and any other binary asset copyconfig.json matches) on every `gulp copy`/`gulp all` run. - Invoice/reminder editor: replace the row's flat action icons (edit/ delete/insert/set-price) with a single context button opening a dropdown menu, reusing the existing $ocms.menu toolbar mechanism. - Fix "-1" leaking into the Pos. column for Text/Title rows: $inv.rrw's `dta.position || dta.SortOrder` fallback treated the numbering's intentional empty string as unset and fell back to SortOrder (-1 for such rows); now only a genuinely uncomputed position falls back. - Zusatzempfänger (CC) is now edited together with the main invoice email in one combined dialog ($inv.emailFrm/$inv.eEmail), wired into all three entry points (create, continue, storno) instead of a separate hover-revealed row with its own edit target. - Add tooltips to every row/header edit control, including several that had none before (header field pencils, the request/section title edit button), phrased as object+verb ($rct.frmTT). Co-Authored-By: Claude Sonnet 5 --- Fuchs/gulpfile.js | 6 +- Fuchs/js/intranet/modules/fis.inv_shared.js | 105 ++++++++++++--- Fuchs/js/intranet/modules/fis.inv_shared.scss | 70 +++++++--- Fuchs/js/intranet/modules/fis.req_txt_de.js | 17 ++- Fuchs/wwwroot/web/fis.inv.css | 64 ++++++--- Fuchs/wwwroot/web/fis.inv.de.js | 122 ++++++++++++++---- Fuchs/wwwroot/web/fis.inv.de.min.js | 2 +- Fuchs/wwwroot/web/fis.inv.min.css | 2 +- Fuchs/wwwroot/web/fis.req.css | 64 ++++++--- Fuchs/wwwroot/web/fis.req.de.js | 122 ++++++++++++++---- Fuchs/wwwroot/web/fis.req.de.min.js | 2 +- Fuchs/wwwroot/web/fis.req.min.css | 2 +- 12 files changed, 447 insertions(+), 131 deletions(-) diff --git a/Fuchs/gulpfile.js b/Fuchs/gulpfile.js index 262547b..acfbfff 100644 --- a/Fuchs/gulpfile.js +++ b/Fuchs/gulpfile.js @@ -147,7 +147,11 @@ gulp.task("copy", async () => { const jobs = []; for (const cpy of copyconfig) { jobs.push( - pipeline(gulp.src(cpy.src, { allowEmpty: true }), gulp.dest(cpy.dest), preservetimeSafe(), logSink("Copied: ")) + /* encoding:false is required — vinyl-fs otherwise UTF-8-decodes every file by default, + which corrupts binary assets (fonts, etc.) by replacing invalid byte sequences with + U+FFFD. This is what silently mangled the glyphicons webfont before (see "Fix + corrupted glyphicons webfont files causing decode failures"). */ + pipeline(gulp.src(cpy.src, { allowEmpty: true, encoding: false }), gulp.dest(cpy.dest), preservetimeSafe(), logSink("Copied: ")) ); } await Promise.all(jobs); diff --git a/Fuchs/js/intranet/modules/fis.inv_shared.js b/Fuchs/js/intranet/modules/fis.inv_shared.js index 10ed336..674bcce 100644 --- a/Fuchs/js/intranet/modules/fis.inv_shared.js +++ b/Fuchs/js/intranet/modules/fis.inv_shared.js @@ -746,13 +746,13 @@ $inv.ccInv = function (ev) { //normale rechnung let admin = response.admin || {}, fm = (cls, txt, nme, hd, dta) => { let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt); - $$.dc('axf', fr).append($$.dc('ibtn edit').data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); + $$.dc('axf', fr).append($$.dc('ibtn edit', { title: $rct.frmTT[nme] || $rct.frm[nme] }).data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); rif.tbl.data('new')[nme] = txt; }; fm('tfrm', admin.invoicetitle, 'invoicetitle', 0, null); fm('adrfrm', admin.invoiceaddress, 'invoiceaddress', 0, null); fm('locfrm', '', 'loc', 1, { list: deepCopy(response.locations), lbl: 'ref', property: 'address' }); - fm('emailfrm', admin.invoiceemail, 'invoiceemail', 0, null); + $inv.emailFrm(frm, rif, admin.invoiceemail, admin.invoiceemailcc); $$.dc('sndfrm', frm).append($$.dc('content').text(admin.sender)); let admtxt, admlbl; @@ -821,13 +821,13 @@ $inv.ccStInv = function (ev) { // Stornorechnung let admin = response.admin || {}, fm = (cls, txt, nme, hd, dta) => { let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt); - $$.dc('axf', fr).append($$.dc('ibtn edit').data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); + $$.dc('axf', fr).append($$.dc('ibtn edit', { title: $rct.frmTT[nme] || $rct.frm[nme] }).data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); rif.tbl.data('new')[nme] = txt; }; fm('tfrm', admin.invoicetitle, 'invoicetitle', 0, null); fm('adrfrm', admin.invoiceaddress, 'invoiceaddress', 0, null); fm('locfrm', admin.provisionlocation, 'loc', 1, { list: deepCopy(response.locations), lbl: 'ref', property: 'address' }); - fm('emailfrm', admin.invoiceemail, 'invoiceemail', 0, null); + $inv.emailFrm(frm, rif, admin.invoiceemail, admin.invoiceemailcc); $$.dc('sndfrm', frm).append($$.dc('content').text(admin.sender)); let admtxt, admlbl; @@ -897,14 +897,13 @@ $inv.cntInv = function (data) { //invoice continuation let fm = (cls, txt, nme, hd, dta) => { let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt); - $$.dc('axf', fr).append($$.dc('ibtn edit').data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); + $$.dc('axf', fr).append($$.dc('ibtn edit', { title: $rct.frmTT[nme] || $rct.frm[nme] }).data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); rif.tbl.data('new')[nme] = txt; }; fm('tfrm', inv.InvoiceTitle, 'invoicetitle', 0, null); fm('adrfrm', inv.SendToAddress, 'invoiceaddress', 0, null); fm('locfrm', inv.ProvisionLocation, 'loc', 1, null); - fm('emailfrm', inv.SendToEmail, 'invoiceemail', 0, null); - fm('emailccfrm', inv.SendToEmailCc, 'invoiceemailcc', 0, null); + $inv.emailFrm(frm, rif, inv.SendToEmail, inv.SendToEmailCc); $$.dc('sndfrm', frm).append($$.dc('content').text(response.admin.sender)); fm('admfrm', inv.ProvisionPeriod, 'provisionperiod', ((inv.ProvisionPeriod || '').includes('-') === true ? $rct.provP : $rct.provD), 1, null); @@ -960,14 +959,13 @@ $inv.cSt = function (data) { let fm = (cls, txt, nme, hd, dta) => { let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt); - $$.dc('axf', fr).append($$.dc('ibtn edit').data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); + $$.dc('axf', fr).append($$.dc('ibtn edit', { title: $rct.frmTT[nme] || $rct.frm[nme] }).data('dialog', $rct.frm[nme]).append(gi('pencil')).click($.extend({ t: fc, nme: nme, change: (nv) => { rif.tbl.data('new')[nme] = nv; } }, dta), $inv.eHtml)); rif.tbl.data('new')[nme] = txt; }; fm('tfrm', inv.InvoiceTitle, 'invoicetitle', 0, null); fm('adrfrm', inv.SendToAddress, 'invoiceaddress', 0, null); fm('locfrm', inv.ProvisionLocation, 'loc', 1, null); - fm('emailfrm', inv.SendToEmail, 'invoiceemail', 0, null); - fm('emailccfrm', inv.SendToEmailCc, 'invoiceemailcc', 0, null); + $inv.emailFrm(frm, rif, inv.SendToEmail, inv.SendToEmailCc); $$.dc('sndfrm', frm).append($$.dc('content').text(response.admin.sender)); fm('admfrm', inv.ProvisionPeriod, 'provisionperiod', ((inv.ProvisionPeriod || '').includes('-') === true ? $rct.provP : $rct.provD), 1, null); @@ -1095,6 +1093,45 @@ $inv.eProvisionPeriod = function (ev) { }); }; +/* Main recipient + CC ("Zusatzempfänger") edited together in one dialog — there is no separate + pencil on the CC row, only on the email row (see $inv.emailFrm), which passes both content + spans through ev.data.t / ev.data.ccT. */ +$inv.eEmail = function (ev) { + let flds = [ + { name: 'email', label: $rct.frm.invoiceemail, type: 'text', value: (ev.data.t || $$.s()).text() }, + { name: 'emailcc', label: $rct.frm.invoiceemailcc, type: 'text', value: (ev.data.ccT || $$.s()).text() } + ]; + $ocms.dlgform(flds, { + title: $rct.frm.invoiceemail, + success: function (res) { + let email = ('' + (res.email || '')).trim(), cc = ('' + (res.emailcc || '')).trim(); + if (ev.data && ev.data.t) { ev.data.t.text(email); } + if (ev.data && ev.data.ccT) { ev.data.ccT.text(cc); } + if (typeof (ev.data || {}).change === 'function') { ev.data.change(email); } + if (typeof (ev.data || {}).changeCc === 'function') { ev.data.changeCc(cc); } + /* backend-authoritative: mirror both fields to the server session (see $inv.eHtml's + generic path, which this mirrors for a single field). */ + $inv.d.syncField('invoiceemail', email); + $inv.rd.syncField('invoiceemail', email); + $inv.d.syncField('invoiceemailcc', cc); + $inv.rd.syncField('invoiceemailcc', cc); + } + }); +}; +/* Builds the email + CC ("Zusatzempfänger") header rows together: only the email row gets a + pencil, opening the combined $inv.eEmail dialog for both fields (see there). */ +$inv.emailFrm = function (frm, rif, email, emailcc) { + let ccfr = $$.dc('inpfrm emailccfrm', frm), ccfc = $$.dc('content', ccfr).rwText(emailcc); + let efr = $$.dc('inpfrm emailfrm', frm), efc = $$.dc('content', efr).rwText(email); + $$.dc('axf', efr).append($$.dc('ibtn edit', { title: $rct.frmTT.invoiceemail }).append(gi('pencil')).click({ + t: efc, ccT: ccfc, nme: 'invoiceemail', + change: (nv) => { rif.tbl.data('new').invoiceemail = nv; }, + changeCc: (nv) => { rif.tbl.data('new').invoiceemailcc = nv; } + }, $inv.eHtml)); + rif.tbl.data('new').invoiceemail = email; + rif.tbl.data('new').invoiceemailcc = emailcc; +}; + $inv.eHtml = function (ev) { /* The recipient address and the service date/period are edited through structured dialogs whenever an invoice draft is active; reminders (no invoice draft token) keep the plain @@ -1103,6 +1140,9 @@ $inv.eHtml = function (ev) { if (ev.data.nme === 'invoiceaddress') { return $inv.eAddress.call(this, ev); } if (ev.data.nme === 'provisionperiod') { return $inv.eProvisionPeriod.call(this, ev); } } + /* The CC/Zusatzempfänger recipient is edited together with the main email, in one dialog — + there is no separate pencil on the CC row (see $inv.emailFrm). */ + if (ev.data && !(ev.data instanceof jQuery) && ev.data.nme === 'invoiceemail' && ev.data.ccT) { return $inv.eEmail.call(this, ev); } let t = $(this), frmct = ev.data instanceof jQuery ? ev.data : ev.data.t; /* Single-line fields must stay plain text — the TinyMCE/html editor wraps the value in

tags, which used to get posted and persisted verbatim (e.g.

18.06.2026

in the @@ -1216,15 +1256,29 @@ $inv.eRw = function(row, dta, flds) { changed block(s) to the backend session (t_fds_inv -> syncChanged). Committing on drop (rather than on every mid-drag hover-swap) avoids rebuilding the row that is currently being dragged. */ $inv.bdysort = (i, e) => { $(e).Sortable({ dragItem: false, dragHandleClass: 'ico', parentident: 'tr', onend: () => { $inv.t_fds_inv(); } }) } +/* Single "Kontext-Button" per row/field, opening a dropdown of the row's edit actions + (reuses $ocms.menu, the same mechanism that already drives the toolbar menus — see + $inv.eM/$inv.iMn). Calling .ocmsmenu() directly on a