Refactor code structure for improved readability and maintainability
This commit is contained in:
+216
-77
@@ -627,11 +627,202 @@ $inv.eM = (r, re, opt) => {
|
||||
if ((opt || '').split(',').includes('setm') === true) {
|
||||
m.push({ lbl: $ict.setm, fnc: $inv.ssetmode });
|
||||
}
|
||||
if ((opt || '').split(',').includes('iss') === true) { /* backend-authoritative draft (ADR 0006) */
|
||||
m.push({ lbl: 'Änderungshistorie', fnc: () => $inv.d.history() });
|
||||
m.push({ lbl: 'Änderungen verwerfen', fnc: () => $inv.d.discard() });
|
||||
}
|
||||
if (booln(r, false) === true) {
|
||||
m.push({ lbl: $ict.rel, fnc: $inv.rReload });
|
||||
}
|
||||
return $('#topbar').ocmsmenu(m); /* empty Array => empty menu */
|
||||
};
|
||||
/* ── Backend-authoritative draft editing controller (ADR 0006/0007) ───────────
|
||||
The server holds the truth for an invoice draft in an in-memory session; this
|
||||
object seeds it, sends single edits as deltas, and renders the totals footer +
|
||||
validation from the authoritative server state. It is invoice-only: reminders
|
||||
never obtain a token (invSumUpdate, the seed trigger, is only bound for invoices),
|
||||
so they keep the legacy stateless flow untouched. */
|
||||
$inv.d = {
|
||||
tbl: () => $('div.invoice_layout table.invi'),
|
||||
layout: () => $('div.invoice_layout'),
|
||||
token: function () { return $inv.d.tbl().data('dtoken') || ''; },
|
||||
/* Hash each assembled block so only genuinely-changed blocks are sent as deltas. */
|
||||
hashes: function () {
|
||||
let bai = $inv.d.tbl().data('bai') || [], h = {};
|
||||
$.each(bai, (i, b) => { h[(b.Id || '').toString()] = JSON.stringify(b); });
|
||||
return h;
|
||||
},
|
||||
/* Seed the authoritative server session from the assembled editor payload. Called
|
||||
once from invSumUpdate on open; thereafter the backend is the source of truth. */
|
||||
seed: function (payload) {
|
||||
let l = $inv.d.layout(); l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
||||
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes());
|
||||
$fis.draft.bind(r.token, {
|
||||
onReady: () => $inv.d.refresh(),
|
||||
onExpiring: (s) => $inv.d.warnExpiry(s),
|
||||
onClosed: (reason) => $inv.d.closed(reason)
|
||||
});
|
||||
$inv.d.refresh();
|
||||
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding'); }
|
||||
});
|
||||
},
|
||||
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
||||
refresh: function (cb) {
|
||||
let t = $inv.d.token(); if (t === '') { return; }
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dstate'), data: { token: t }, success: (state) => {
|
||||
$inv.d.applyState(state); if (typeof cb === 'function') { cb(state); }
|
||||
}, error: (xhr) => { if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } },
|
||||
complete: () => { $inv.d.layout().rC('freeze'); }
|
||||
});
|
||||
},
|
||||
applyState: function (state) {
|
||||
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
||||
tbl.data('dver', state.version).data('serverSums', state.sums);
|
||||
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
||||
$inv.d.validation(state.validation || []);
|
||||
},
|
||||
/* Send one change to the server; the draftReady signal and this success both refresh. */
|
||||
sync: function (delta) {
|
||||
let t = $inv.d.token(); if (t === '') { return; }
|
||||
$inv.d.layout().aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
||||
success: () => { $inv.d.refresh(); },
|
||||
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
||||
});
|
||||
},
|
||||
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
||||
changed/removed blocks as granular block.replace / block.remove deltas. */
|
||||
syncChanged: function (tbl) {
|
||||
if (($inv.d.token()) === '') { return; }
|
||||
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [];
|
||||
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
||||
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
||||
tbl.data('dhashes', next);
|
||||
changed.forEach((b) => $inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b }));
|
||||
removed.forEach((id) => $inv.d.sync({ Target: 'block.remove', Ref: id }));
|
||||
},
|
||||
/* Map an inline recipient field to its delta target and send it. */
|
||||
syncField: function (nme, val) {
|
||||
if ($inv.d.token() === '') { return; }
|
||||
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
||||
let target = map[nme]; if (!target) { return; }
|
||||
$inv.d.sync({ Target: target, Value: val });
|
||||
},
|
||||
/* Render the totals footer from the server sums (port of invSumUpdate's footer half). */
|
||||
footer: function (tbl, sums, admin) {
|
||||
let ft = tbl.children('tfoot').empty(); tbl.nextAll('.fnote').remove();
|
||||
let p13b = bool(admin.p13b, false);
|
||||
let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst));
|
||||
let fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t);
|
||||
rwcy('Netto', sums.total_net || 0);
|
||||
if (p13b === false) { $.each(sums.vat || {}, (rate, amt) => rwcy($rct.vat + ' ' + rate + '%', amt, 'tvat')); }
|
||||
rwcy('Summe', sums.total_gross || 0);
|
||||
let itype = (admin.type || '');
|
||||
if (itype === 'i') { fn($rct.note2); fn($rct.note4); }
|
||||
else if (itype === 'c') { fn($rct.note2); }
|
||||
else {
|
||||
fn(string($rct.note3, [fnum(((sums.service_net || 0) + (sums.service_vat || 0)) * (admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax');
|
||||
fn($rct.note2);
|
||||
fn(string($rct.note1, [fnum((sums.service_net || 0) + (sums.service_vat || 0), $rct.cst), fnum(sums.service_net || 0, $rct.cst), fnum(sums.service_vat || 0, $rct.cst)]));
|
||||
}
|
||||
if (p13b === true) { fn($rct.note13b); }
|
||||
},
|
||||
validation: function (msgs) {
|
||||
let frm = $('div.invoice_layout'); if (frm.length < 1) { return; }
|
||||
let box = frm.children('.dvalidation');
|
||||
if (box.length < 1) { box = $$.dc('dvalidation'); frm.prepend(box); }
|
||||
box.empty().tC('hidden', (msgs || []).length < 1);
|
||||
$.each(msgs || [], (i, m) => $$.dc('dvmsg', box).aC(m.severity).text(m.message));
|
||||
},
|
||||
/* PDF preview straight from the cache; confirm = flush + finalise, cancel = discard. */
|
||||
preview: function () {
|
||||
let t = $inv.d.token(); if (t === '') { return; }
|
||||
let l = $inv.d.layout();
|
||||
let email = ($inv.d.tbl().data('new') || {}).invoiceemail || '';
|
||||
if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } }
|
||||
l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dpreview'), data: { token: t }, success: (response) => {
|
||||
l.rC('freeze');
|
||||
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), total = response.total;
|
||||
if (total > 10) { $$.dc('note warn', c).text($ict.tpe); }
|
||||
$.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); });
|
||||
for (let ic = (response.img || []).length + 1; ic <= total; ic++) { $$.dc('pdfp ph', c).append($$.dc('note', $ict.pna)); }
|
||||
$ocms.dlg(c, {
|
||||
size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $rct.crI,
|
||||
confirm: function (e) {
|
||||
let ct = $(this); l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dsave'), data: { token: t }, success: (sv) => {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('req/sconf'), data: { id: sv.invid }, success: (cresp) => {
|
||||
ct.trigger('modal_close');
|
||||
if (cresp.hasFile === true) { window.open($ocms.url('req/idoc') + '?id=' + sv.invid, '_blank'); }
|
||||
$inv.d.close();
|
||||
$ocms.init('req'); $inv.rReload();
|
||||
}, error: () => { alert($t.f1); ct.trigger('modal_close'); }, complete: () => { l.rC('freeze'); }
|
||||
});
|
||||
}, error: () => { l.rC('freeze'); alert($ict.eis); }
|
||||
});
|
||||
},
|
||||
cancel: function (e) { if (confirm($ict.cdI)) { $inv.d.close(); $inv.rReload(); } }
|
||||
});
|
||||
}, error: () => { l.rC('freeze'); alert($ict.eis); }
|
||||
});
|
||||
},
|
||||
/* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */
|
||||
save: function () {
|
||||
let t = $inv.d.token(); if (t === '') { return; }
|
||||
let l = $inv.d.layout(); l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dsave'), data: { token: t }, success: (r) => { $inv.d.tbl().data('invid', r.invid); },
|
||||
error: () => { alert($ict.eis); }, complete: () => { l.rC('freeze'); }
|
||||
});
|
||||
},
|
||||
history: function () {
|
||||
let t = $inv.d.token(); if (t === '') { return; }
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('inv/dhistory'), data: { token: t }, success: (r) => {
|
||||
let c = $$.dc('dhist');
|
||||
if ((r.history || []).length < 1) { $$.dc('note', c).text('Noch keine Änderungen erfasst.'); }
|
||||
else {
|
||||
let ts = $$.tblset({ class: 'invtbl fullwidth' }, c);
|
||||
$$.tr(ts.hd).append([$$.th().text('Zeit'), $$.th().text('Feld'), $$.th().text('Alt'), $$.th().text('Neu')]);
|
||||
$.each(r.history, (i, h) => $$.tr(ts.bdy).append([$$.tdc('keep', fdt(h.timestamp)), $$.td().text(h.target), $$.td().text(h.oldValue), $$.td().text(h.newValue)]));
|
||||
}
|
||||
$ocms.dlg(c, { width: 800, form: false });
|
||||
}
|
||||
});
|
||||
},
|
||||
discard: function () {
|
||||
let invid = $inv.d.tbl().data('invid') || '';
|
||||
if (invid === '') { alert('Es wurde noch kein Zwischenstand gespeichert, der wiederhergestellt werden könnte.'); return; }
|
||||
if (confirm('Alle Änderungen verwerfen und den zuletzt gespeicherten Stand neu laden?') === false) { return; }
|
||||
$inv.d.close();
|
||||
$inv.cntInv({ id: invid });
|
||||
},
|
||||
warnExpiry: function (secondsLeft) {
|
||||
let mins = Math.max(1, Math.round((secondsLeft || 0) / 60));
|
||||
$fis.notifications.push({ severity: 'info', title: 'Entwurf läuft ab', message: 'Der Rechnungsentwurf läuft in etwa ' + mins + ' Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren.' });
|
||||
},
|
||||
closed: function (reason) {
|
||||
let t = $inv.d.token();
|
||||
$inv.d.tbl().removeData('dtoken');
|
||||
if (t !== '') { $fis.draft.release(t); }
|
||||
$fis.frm_edit().remove(); $fis.lf(true);
|
||||
$fis.notifications.push({ severity: 'error', title: 'Entwurf geschlossen', message: reason === 'expired' ? 'Der Rechnungsentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.' : 'Der Rechnungsentwurf wurde geschlossen.' });
|
||||
try { $inv.rReload(); } catch (e) { }
|
||||
},
|
||||
close: function () {
|
||||
let t = $inv.d.token();
|
||||
if (t !== '') { $ocms.postXT({ url: $ocms.url('inv/dclose'), data: { token: t } }); $fis.draft.release(t); }
|
||||
$inv.d.tbl().removeData('dtoken');
|
||||
}
|
||||
};
|
||||
$inv.cInv2 = function (data) {
|
||||
let fr = $$.dc('rfrm').ldng(1);
|
||||
let o = $ocms.dlg(fr, { width: 1000 });
|
||||
@@ -1027,6 +1218,8 @@ $inv.eHtml = function (ev) {
|
||||
if (typeof change === 'function') {
|
||||
change(response.txt);
|
||||
}
|
||||
/* backend-authoritative: mirror the inline recipient-field edit to the server session */
|
||||
$inv.d.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt);
|
||||
},
|
||||
tinymce: { valid_elements: 'br', hidemenu: true, hidetoolbar: true }
|
||||
}
|
||||
@@ -1241,6 +1434,13 @@ $inv.invSumUpdate = function () {
|
||||
}
|
||||
tbl.data('sms', sms);
|
||||
tbl.data('bai', ba);
|
||||
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
||||
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
||||
model to the server session and switch to server-driven totals from then on. */
|
||||
if ((tbl.data('dtoken') || '') === '' && bool(tbl.data('dseeding'), false) === false && ((tbl.data('admin') || {}).type != null)) {
|
||||
tbl.data('dseeding', true);
|
||||
$inv.d.seed($.extend($inv.invcPayload(tbl.data()), { invid: tbl.data('invid') || '' }));
|
||||
}
|
||||
};
|
||||
$inv.worknotes = function (rx) {
|
||||
let wn = '';
|
||||
@@ -1328,7 +1528,13 @@ $inv.rendersrq = function () {
|
||||
$$.tdc('currency isumval', istr);
|
||||
}
|
||||
};
|
||||
$inv.t_fds_inv = () => { $('div.invoice_layout table.invi').trigger('fds.inv'); };
|
||||
$inv.t_fds_inv = () => {
|
||||
let tbl = $('div.invoice_layout table.invi');
|
||||
tbl.trigger('fds.inv');
|
||||
/* After any local item mutation, push the changed block(s) to the authoritative
|
||||
server session as granular deltas (invoice drafts only — reminders have no token). */
|
||||
if ((tbl.data('dtoken') || '') !== '') { $inv.d.syncChanged(tbl); }
|
||||
};
|
||||
$inv.sedit = () => {
|
||||
$inv.sprev(true);
|
||||
};
|
||||
@@ -1383,6 +1589,7 @@ $inv.sp13b = () => {
|
||||
} else {
|
||||
}
|
||||
tbl.trigger('fds.inv');
|
||||
$inv.d.sync({ Target: 'p13b', Value: d.admin.p13b });
|
||||
};
|
||||
/* Maps an item row's data to the backend item contract consumed by InvoiceSetPricing
|
||||
/ FuchsPdf.ApplyInvoice: { id, type, title (plain), desc (html), qty, price_net,
|
||||
@@ -1437,6 +1644,7 @@ $inv.setSetmode = (mode) => {
|
||||
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
|
||||
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
|
||||
d.inv.InvoiceOptions = opts.join(',');
|
||||
$inv.d.sync({ Target: 'setmode', Value: mode });
|
||||
};
|
||||
$inv.sctp = () => {
|
||||
let flds = $invcol.ctp;
|
||||
@@ -1453,6 +1661,7 @@ $inv.sctp = () => {
|
||||
cvo.contactEmail = response.email;
|
||||
d.new.CustomValues = JSON.stringify(cvo); //Assign it to new Values so that this is submitted also
|
||||
l.find('.ctpfrm').text(ne(response.name, response.email));
|
||||
$inv.d.sync({ Target: 'contact', Value: { name: response.name, email: response.email } });
|
||||
|
||||
}, typedvalues: true
|
||||
});
|
||||
@@ -1477,82 +1686,12 @@ $inv.invcPayload = function (d) {
|
||||
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
||||
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
|
||||
};
|
||||
$inv.ssave = () => {
|
||||
var l = $('div.invoice_layout'), d = l.find('table.invi').data();
|
||||
$inv.t_fds_inv();
|
||||
l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('req/save'), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid || '' }, success: (response) => {
|
||||
$inv.cntInv({ id: response.id });
|
||||
}, error: () => {
|
||||
alert($ict.eis);
|
||||
}, complete: () => {
|
||||
l.rC('freeze');
|
||||
}
|
||||
});
|
||||
};
|
||||
$inv.sprev = (change) => {
|
||||
var l = $('div.invoice_layout'), d = l.find('table.invi').data();
|
||||
change = bool(change, false);
|
||||
$inv.t_fds_inv();
|
||||
l.aC('freeze');
|
||||
//console.debug({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new });
|
||||
if ($fis.ValidateEmail(d.new.invoiceemail || '') === false) {
|
||||
if (bool(confirm($ict.ivE + $ict.ivEc),false) === false) {
|
||||
l.rC('freeze');
|
||||
return;
|
||||
}
|
||||
}
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid ||'' }, success: (response) => {
|
||||
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id, invtp = response.total;
|
||||
if (invtp > 10) {
|
||||
$$.dc('note warn', c).text($ict.tpe);
|
||||
}
|
||||
$.each(response.img || [], function (ii, img) {
|
||||
$$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px'));
|
||||
});
|
||||
for (let ic = (response.img || []).length + 1; ic <= invtp; ic++) {
|
||||
$$.dc('pdfp ph', c).append($$.dc('note', $ict.pna));
|
||||
}
|
||||
$ocms.dlg(c, {
|
||||
size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $rct.crI, confirm: function (e) {
|
||||
let ct = $(this);
|
||||
l.aC('freeze'); /* spinner while the invoice is finalized/emailed on the backend */
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('req/sconf'), data: { id: invid }, success: (cresp) => {
|
||||
ct.trigger('modal_close');
|
||||
if (cresp.hasFile === true) {
|
||||
window.open($ocms.url('req/idoc') + '?id=' + invid, '_blank'); /* open pdf in new tab, only if a file was actually created */
|
||||
}
|
||||
$ocms.init('req'); /* go back to request list */
|
||||
$inv.rReload();
|
||||
}, error: () => {
|
||||
alert($t.f1);
|
||||
ct.trigger('modal_close');
|
||||
}, complete: () => {
|
||||
l.rC('freeze');
|
||||
}
|
||||
});
|
||||
}, cancel: function (e) {
|
||||
let ct = $(this);
|
||||
if (confirm($ict.cdI)) {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('req/sdel'), data: {
|
||||
id: invid
|
||||
}
|
||||
});
|
||||
}
|
||||
$inv.rReload();
|
||||
}
|
||||
});
|
||||
}, error: () => {
|
||||
alert($ict.eis);
|
||||
}, complete: () => {
|
||||
l.rC('freeze');
|
||||
}
|
||||
});
|
||||
};
|
||||
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
||||
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
||||
token. See $inv.d.save / $inv.d.preview. The finalise + email path (req/sconf) is
|
||||
unchanged and is invoked from the preview modal's confirm handler. */
|
||||
$inv.ssave = () => { $inv.d.save(); };
|
||||
$inv.sprev = (change) => { $inv.d.preview(); };
|
||||
$inv.rReload = () => {
|
||||
try {
|
||||
let s = $('#listframe ul.rql:first').data();
|
||||
|
||||
Reference in New Issue
Block a user