Changed Reminder Systematics analogue to invoices
This commit is contained in:
+175
-46
@@ -841,6 +841,161 @@ $inv.d = {
|
||||
$inv.d.tbl().removeData('dtoken');
|
||||
}
|
||||
};
|
||||
/* ── Backend-authoritative reminder draft editing (ADR 0006/0007) ─────────────
|
||||
The reminder mirror of $inv.d: the server holds the truth for a reminder draft in
|
||||
an in-memory session; this object seeds it (rem/dopen), sends single edits as deltas
|
||||
(rem/dpatch), and renders the open-amount footer + validation from the authoritative
|
||||
server state (rem/dstate). Preview renders straight from the cache (rem/dpreview);
|
||||
confirm flushes (rem/dsave) then finalises + emails (rem/conf). It coexists with $inv.d
|
||||
on the same DOM: each keys off its own token (rdtoken vs dtoken), so the shared inline
|
||||
editor safely no-ops for the mode that is not active. */
|
||||
$inv.rd = {
|
||||
tbl: () => $('div.invoice_layout table.invi'),
|
||||
layout: () => $('div.invoice_layout'),
|
||||
token: function () { return $inv.rd.tbl().data('rdtoken') || ''; },
|
||||
/* Seed the authoritative server session from the assembled reminder editor payload. */
|
||||
seed: function (payload) {
|
||||
let l = $inv.rd.layout(); l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
||||
$inv.rd.tbl().data('rdtoken', r.token).data('rdver', r.version);
|
||||
$fis.draft.bind(r.token, {
|
||||
onReady: () => $inv.rd.refresh(),
|
||||
onExpiring: (s) => $inv.rd.warnExpiry(s),
|
||||
onClosed: (reason) => $inv.rd.closed(reason)
|
||||
});
|
||||
$inv.rd.refresh();
|
||||
}, error: () => { l.rC('freeze'); }
|
||||
});
|
||||
},
|
||||
/* Re-fetch the authoritative state and render the open-amount footer + validation from it. */
|
||||
refresh: function (cb) {
|
||||
let t = $inv.rd.token(); if (t === '') { return; }
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/dstate'), data: { token: t }, success: (state) => {
|
||||
$inv.rd.applyState(state); if (typeof cb === 'function') { cb(state); }
|
||||
}, error: (xhr) => { if (xhr && xhr.status === 410) { $inv.rd.closed('expired'); } },
|
||||
complete: () => { $inv.rd.layout().rC('freeze'); }
|
||||
});
|
||||
},
|
||||
applyState: function (state) {
|
||||
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
||||
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
||||
$inv.rd.footer(tbl, state.sums || {});
|
||||
$inv.rd.validation(state.validation || []);
|
||||
},
|
||||
/* Send one change to the server; the draftReady signal and this success both refresh. */
|
||||
sync: function (delta) {
|
||||
let t = $inv.rd.token(); if (t === '') { return; }
|
||||
$inv.rd.layout().aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
||||
success: () => { $inv.rd.refresh(); },
|
||||
error: (xhr) => { $inv.rd.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.rd.closed('expired'); } }
|
||||
});
|
||||
},
|
||||
/* Map an inline recipient field to its delta target and send it. */
|
||||
syncField: function (nme, val) {
|
||||
if ($inv.rd.token() === '') { return; }
|
||||
let map = { subject: 'subject', invoiceaddress: 'address', invoiceemail: 'email', text: 'text' };
|
||||
let target = map[nme]; if (!target) { return; }
|
||||
$inv.rd.sync({ Target: target, Value: val });
|
||||
},
|
||||
/* Amount / amount-paid come from the item-row dialog; send both as their own deltas. */
|
||||
syncAmount: function (amount, amount_payed) {
|
||||
if ($inv.rd.token() === '') { return; }
|
||||
$inv.rd.sync({ Target: 'amount', Value: (amount != null ? amount : 0).toString() });
|
||||
$inv.rd.sync({ Target: 'amount_payed', Value: (amount_payed != null ? amount_payed : 0).toString() });
|
||||
},
|
||||
/* Render the open-amount footer from the server sums. */
|
||||
footer: function (tbl, sums) {
|
||||
let ft = tbl.children('tfoot').empty();
|
||||
let tr = $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 3 }).text('Offener Betrag')]);
|
||||
$$.tdc('currency', tr, fnum(sums.amount_open || 0, $rct.cst));
|
||||
},
|
||||
validation: function (msgs) {
|
||||
let frm = $inv.rd.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 + email, cancel = discard. */
|
||||
preview: function () {
|
||||
let t = $inv.rd.token(); if (t === '') { return; }
|
||||
let l = $inv.rd.layout();
|
||||
let email = (($inv.rd.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('rem/dpreview'), data: { token: t }, success: (response) => {
|
||||
l.rC('freeze');
|
||||
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88);
|
||||
$.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); });
|
||||
$ocms.dlg(c, {
|
||||
size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $ict.remd,
|
||||
confirm: function (e) {
|
||||
let ct = $(this); l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/dsave'), data: { token: t }, success: (sv) => {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/conf'), data: { id: sv.remid }, success: () => {
|
||||
ct.trigger('modal_close');
|
||||
window.open($ocms.url('rem/idoc') + '?id=' + sv.remid, '_blank');
|
||||
$inv.rd.close();
|
||||
$ocms.init('req'); $inv.rReload();
|
||||
}, error: () => { alert($t.f1); ct.trigger('modal_close'); }, complete: () => { l.rC('freeze'); }
|
||||
});
|
||||
}, error: () => { l.rC('freeze'); alert($t.f1); }
|
||||
});
|
||||
},
|
||||
cancel: function (e) { if (confirm($ict.cdI)) { $inv.rd.close(); $inv.rReload(); } }
|
||||
});
|
||||
}, error: () => { l.rC('freeze'); alert($t.f1); }
|
||||
});
|
||||
},
|
||||
/* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */
|
||||
save: function () {
|
||||
let t = $inv.rd.token(); if (t === '') { return; }
|
||||
let l = $inv.rd.layout(); l.aC('freeze');
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/dsave'), data: { token: t }, success: (r) => { $inv.rd.tbl().data('remid', r.remid); },
|
||||
error: () => { alert($t.f1); }, complete: () => { l.rC('freeze'); }
|
||||
});
|
||||
},
|
||||
history: function () {
|
||||
let t = $inv.rd.token(); if (t === '') { return; }
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
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 Mahnentwurf läuft in etwa ' + mins + ' Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren.' });
|
||||
},
|
||||
closed: function (reason) {
|
||||
let t = $inv.rd.token();
|
||||
$inv.rd.tbl().removeData('rdtoken');
|
||||
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 Mahnentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.' : 'Der Mahnentwurf wurde geschlossen.' });
|
||||
try { $inv.rReload(); } catch (e) { }
|
||||
},
|
||||
close: function () {
|
||||
let t = $inv.rd.token();
|
||||
if (t !== '') { $ocms.postXT({ url: $ocms.url('rem/dclose'), data: { token: t } }); $fis.draft.release(t); }
|
||||
$inv.rd.tbl().removeData('rdtoken');
|
||||
}
|
||||
};
|
||||
$inv.cInv2 = function (data) {
|
||||
let fr = $$.dc('rfrm').ldng(1);
|
||||
let o = $ocms.dlg(fr, { width: 1000 });
|
||||
@@ -1239,8 +1394,11 @@ $inv.eHtml = function (ev) {
|
||||
if (typeof change === 'function') {
|
||||
change(response.txt);
|
||||
}
|
||||
/* backend-authoritative: mirror the inline recipient-field edit to the server session */
|
||||
/* backend-authoritative: mirror the inline recipient-field edit to the server session.
|
||||
Invoice and reminder editors share this DOM; each syncField no-ops unless its own
|
||||
draft token is present, so only the active mode's session receives the delta. */
|
||||
$inv.d.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt);
|
||||
$inv.rd.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt);
|
||||
},
|
||||
tinymce: { valid_elements: 'br', hidemenu: true, hidetoolbar: true }
|
||||
}
|
||||
@@ -1895,6 +2053,8 @@ $inv.eRowR = function (ev) {
|
||||
$.extend(tdta.rm, res);
|
||||
tbl.data(tdta);
|
||||
$inv.rRemRw.call(row, tdta);
|
||||
/* backend-authoritative: mirror the edited amount / amount-paid to the server session */
|
||||
$inv.rd.syncAmount(tdta.rm.amount, tdta.rm.amount_payed);
|
||||
}, typedvalues: true
|
||||
});
|
||||
};
|
||||
@@ -1935,57 +2095,26 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation
|
||||
rif.tbl.children('tbody').each($inv.bdysort);
|
||||
rif.tbl.trigger('fds.inv'); /* trigger calculations */
|
||||
|
||||
/* Seed the authoritative server session (ADR 0006). Amounts join the recipient
|
||||
fields in the 'new' block; the reference invoice data goes into 'rem'. From here
|
||||
the backend owns the open-amount computation and validation; inline edits and the
|
||||
item-row dialog post single deltas (see $inv.rd). */
|
||||
let nw = rif.tbl.data('new');
|
||||
nw.amount = rem.amount; nw.amount_payed = rem.amount_payed;
|
||||
$inv.rd.seed({
|
||||
rem: { invid: rem.invid, type: rem.type, invoiceid: rem.invoiceid, invoicedate: rem.invoicedate },
|
||||
new: nw
|
||||
});
|
||||
}, complete: () => {
|
||||
//o.c.trigger('modal_close');
|
||||
}
|
||||
});
|
||||
};
|
||||
$inv.rprev = () => {
|
||||
var l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||||
$.extend(d.new, tbl.find('tbody > tr:first').data());
|
||||
l.aC('freeze');
|
||||
//console.debug({ rem: d.rm, 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('rem/prep'), data: { remc: JSON.stringify({ rem: d.rm, new: d.new }), id: d.invid || '' }, success: (response) => {
|
||||
l.rC('freeze');
|
||||
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), remid = response.id;
|
||||
$.each(response.img || [], function (ii, img) {
|
||||
$$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px'));
|
||||
});
|
||||
$ocms.dlg(c, {
|
||||
size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $ict.remd, confirm: function (e) {
|
||||
let ct = $(this);
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/conf'), data: { id: remid }, success: () => {
|
||||
ct.trigger('modal_close');
|
||||
window.open($ocms.url('rem/idoc') + '?id=' + remid, '_blank'); /* open pdf in new tab */
|
||||
$ocms.init('req'); /* go back to request list */
|
||||
$inv.rReload();
|
||||
}, error: () => {
|
||||
alert($t.f1);
|
||||
ct.trigger('modal_close');
|
||||
}
|
||||
});
|
||||
}, cancel: function (e) {
|
||||
let ct = $(this);
|
||||
if (confirm($ict.cdI)) {
|
||||
$ocms.postXT({
|
||||
url: $ocms.url('rem/del'), data: {
|
||||
id: remid
|
||||
}
|
||||
});
|
||||
}
|
||||
$inv.rReload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
/* Preview + finalise now run through the backend-authoritative session ($inv.rd):
|
||||
the PDF renders straight from the server cache (no rem/prep DB write), and confirm
|
||||
flushes (rem/dsave) then finalises + emails (rem/conf). */
|
||||
$inv.rd.preview();
|
||||
};
|
||||
$inv.sis = (id) => {
|
||||
if (confirm($ict.sisc)) {
|
||||
|
||||
Reference in New Issue
Block a user