- Introduced comprehensive unit tests for the Fuchs_DataService library, covering DATEV header formatting, CSV/XML generation, and FdsMfrClient construction. - Implemented tests for FdsMfr.UpdateNeed parsing and FdsShared utility helpers, ensuring correct functionality and stability. - Added tests for FdsConfig and FdsMfrClient to validate configuration resolution and client construction. Document decisions on backend-authoritative invoice and reminder handling - Created ADR 0008 to clarify that all invoice types and reminder stages are backend-authoritative during drafting and previewing. - Established that all calculations and settings must be processed server-side, ensuring consistency between online editor and PDF outputs. Define irreversible mutations for set-price modes in invoices - Documented ADR 0009 to specify that the "Set mit Preis" and "Nur Set mit Preis" operations are irreversible mutations affecting service request blocks. - Clarified that these operations are not display toggles but actual data changes, ensuring clear expectations for invoice handling. Transition MFR ERP sync to in-process execution within the web app - Created ADR 0010 to outline the migration of Fuchs_DataService from a standalone service to an in-process library within the Fuchs web application. - Updated configuration and logging management to be handled by the host application, streamlining the sync process. Add publish profile and periodic hosted service for job scheduling - Introduced a publish profile for deployment to a specified folder. - Implemented PeriodicHostedService to manage multiple independent jobs, including the MFR ERP sync, with configurable execution intervals. Add dotnet-tools.json for EF Core CLI tools - Included dotnet-tools.json to manage the version of dotnet-ef for Entity Framework Core migrations and commands.
1749 lines
95 KiB
JavaScript
1749 lines
95 KiB
JavaScript
|
||
$inv.cInv = function (ev) {
|
||
let tr = $(this).closest('tr');
|
||
ev.stopPropagation();
|
||
if (tr.is('.selected') === false) {
|
||
return;
|
||
} else if ($fis.isAuth('fds_inv', 2) === false) {
|
||
return;
|
||
}
|
||
$inv.cInv2({ id: ev.data.id });
|
||
};
|
||
$inv.rMn = (o) => {
|
||
let m = [{ lbl: $ict.req, itm: [] }];
|
||
if (bool(o, false) === true && $fis.isAuth('fds_inv', 2) === true) {
|
||
Array.prototype.push.apply(m[0].itm, [
|
||
{ lbl: $rct.crI, fnc: $inv.ccInv, data: { typ: 'r' } }
|
||
, { lbl: $rct.crII, fnc: $inv.ccInv, data: { typ: 'i' } }
|
||
]);
|
||
}
|
||
m.push({ lbl: $ict.rel, fnc: $inv.rReload });
|
||
return $('#topbar').ocmsmenu(m);
|
||
};
|
||
$inv.iMnr = (ix) => {
|
||
let f = booln(ix.isFinal, true), id = ix.Id, fds = booln(ix.fds, false);
|
||
let m = [
|
||
{ glyph: 'glyphicon-menu-left', fnc: () => { $fis.frm_edit().remove(); } },
|
||
{ lbl: $ict.inv, itm: [] }, { lbl: $ict.rem, itm: [] }
|
||
];
|
||
if (f === false && $fis.isAuth('fds_inv', 2) === true) {
|
||
m[1].itm.push({ lbl: $ict.ced, fnc: $inv.clCntInv });
|
||
} else if ($fis.isAuth('fds_inv', 1) === true) {
|
||
m[1].itm.push({ lbl: $ict.dsp, fnc: () => $inv.disp(id, 'inv') })
|
||
}
|
||
if (fds === true && f === true && $fis.isAuth('fds_inv', 2) === true) {
|
||
m[1].itm.push({ lbl: $ict.storno, fnc: () => $inv.storno(id, fds) });
|
||
m[1].itm.push({ lbl: $ict.credit, fnc: () => $inv.credit(id, fds) });
|
||
}
|
||
if (f === true && $fis.isAuth('fds_reminder', 2) === true) {
|
||
m[2].itm.push({ lbl: $ict.remd, fnc: () => $inv.ccRem(id, ix.InvoiceId) });
|
||
m[2].itm.push({ lbl: $ict.remlst, fnc: () => $inv.dspRem(id) });
|
||
}
|
||
if (f === true && $fis.isAuth('fds_reminder', 2) === true && booln(ix.IsSent, false) === false) {
|
||
m[2].itm.push({ lbl: $ict.srs, fnc: () => $inv.srs(id) });
|
||
}
|
||
m.push({ lbl: $ict.rel, fnc: $inv.rReload });
|
||
return $('#topbar').ocmsmenu(m);
|
||
};
|
||
$inv.iMn = (ix) => {
|
||
let f = booln(ix.isFinal, true), id = ix.Id, fds = booln(ix.fds, false);
|
||
let m = [{ glyph: 'glyphicon-menu-left', fnc: () => { $fis.frm_edit().remove(); } },
|
||
{ lbl: $ict.inv, itm: [] }, { lbl: $ict.rem, itm: [] }
|
||
];
|
||
if (f === false && $fis.isAuth('fds_inv', 2) === true) {
|
||
m[1].itm.push({ lbl: $ict.ced, fnc: () => { $inv.cntInv({ id: id }); } });
|
||
} else if ( $fis.isAuth('fds_inv', 1) === true) {
|
||
m[1].itm.push({ lbl: $ict.dsp, fnc: () => $inv.disp(id, 'inv') })
|
||
}
|
||
if ($fis.isAuth('fds_inv', 2) === true) {
|
||
m[1].itm.push({ lbl: $ict.storno, fnc: () => $inv.storno(id, fds) })
|
||
m[1].itm.push({ lbl: $ict.credit, fnc: () => $inv.credit(id, fds) });
|
||
}
|
||
if (f === true && booln(ix.IsPayed, false) === false) {
|
||
if ($fis.isAuth('fds_reminder', 2) === true) {
|
||
m[2].itm.push({ lbl: $ict.remd, fnc: () => $inv.ccRem(id, ix.InvoiceId) });
|
||
}
|
||
if ($fis.isAuth('fds_inv', 2) === true) {
|
||
m[1].itm.push({ lbl: $ict.setpyd, fnc: () => $inv.setPyd(id) });
|
||
}
|
||
} else if (f === true && booln(ix.IsPayed, false) === true && (ix.PaymentStatus || '') === 'm') {
|
||
if ($fis.isAuth('fds_inv', 2) === true) {
|
||
m[1].itm.push({ lbl: $ict.setupd, fnc: () => $inv.setUpd(id) });
|
||
}
|
||
}
|
||
if ($fis.isAuth('fds_reminder', 2) === true) {
|
||
m[2].itm.push({ lbl: $ict.remlst, fnc: () => $inv.dspRem(id) });
|
||
}
|
||
if (f === true && $fis.isAuth('fds_inv', 2) === true && booln(ix.IsSent, false) === false) {
|
||
m[1].itm.push({ lbl: $ict.sis, fnc: () => $inv.sis(id) });
|
||
}
|
||
if (fds === false) {
|
||
m[1].itm.push({ lbl: $ict.mfr, fnc: () => $inv.mfrrel(id) });
|
||
}
|
||
return $('#topbar').ocmsmenu(m);
|
||
}
|
||
/* Menu option CSV builder: 'setm' (Set-Preisanzeige) is only offered while the invoice has not
|
||
yet been explicitly switched to a set-display mode (admin.setmode empty/undefined) — once set,
|
||
changing it again is not supported (the user edits the line items directly instead); it
|
||
reappears only if 'Änderungen verwerfen' reverts to a DB state that was never switched. */
|
||
$inv.emOpt = (admin) => 'iss,p13b,ctp' + (((admin || {}).setmode || '') === '' ? ',setm' : '');
|
||
$inv.eM = (r, re, opt) => {
|
||
let m = [];
|
||
if (booln(r, false) === true || booln(re, false) === true) {
|
||
m.push({ glyph: 'glyphicon-menu-left', fnc: () => { $fis.lf(true); $fis.frm_edit().remove(); } });
|
||
}
|
||
if ((opt || '').split(',').includes('iss') === true) {
|
||
m.push({ lbl: $ict.iss, fnc: $inv.ssave });
|
||
}
|
||
if ((opt || '').split(',').includes('ctp') === true) {
|
||
m.push({ lbl: $ict.ctp, fnc: $inv.sctp });
|
||
}
|
||
if ((opt || '').split(',').includes('p13b') === true) {
|
||
m.push({ lbl: $ict.p13b, fnc: $inv.sp13b });
|
||
}
|
||
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.
|
||
The in-flight ajax promise is stashed on 'dseedpromise' so a delta sent (e.g. via
|
||
a context-menu action) before seeding has actually completed can wait for the token
|
||
instead of silently no-op'ing (see $inv.d.sync). */
|
||
seed: function (payload) {
|
||
let l = $inv.d.layout(); l.aC('freeze');
|
||
let p = $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()).data('dorder', $inv.d.order());
|
||
$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').removeData('dseedpromise'); }
|
||
});
|
||
$inv.d.tbl().data('dseedpromise', p);
|
||
return p;
|
||
},
|
||
/* 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; }
|
||
/* Concurrent refreshes (one per flushed block plus one for a follow-up delta such as
|
||
item.setprice, and SignalR draftReady on top) can have their dstate responses arrive
|
||
out of order. Applying a response older than the version already rendered would
|
||
silently roll back a just-applied server-side mutation (e.g. a set-price conversion
|
||
reverting to its pre-conversion 0/individually-priced state) — so stale responses are
|
||
dropped here instead of rendered. */
|
||
let curVer = tbl.data('dver');
|
||
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||
return;
|
||
}
|
||
tbl.data('dver', state.version).data('serverSums', state.sums);
|
||
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
||
$inv.d.notes(state.notes || []);
|
||
$inv.d.validation(state.validation || []);
|
||
$inv.d.applyItems(tbl, state.req || []);
|
||
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
||
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
||
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
||
a DB state that was never switched (ADR 0006 backend-authoritative refresh). */
|
||
$inv.eM(false, true, $inv.emOpt(state.admin || {}));
|
||
},
|
||
/* Renders the backend-authoritative notice paragraphs (ADR: notice texts are
|
||
backend-authoritative) exactly as FuchsPdf.BuildInvoiceNotes emits them for the PDF,
|
||
so the online editor always shows precisely what will be printed. */
|
||
notes: function (notes) {
|
||
let frm = $inv.d.layout(); if (frm.length < 1) { return; }
|
||
let box = frm.children('.dnotes');
|
||
if (box.length < 1) { box = $$.dc('dnotes'); frm.append(box); }
|
||
box.empty();
|
||
(notes || []).forEach((n) => $$.dc('dnote').aC(n.style || 'InvoiceNotes').text(n.text));
|
||
},
|
||
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
||
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
||
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
||
/* Reconciles each block's rendered rows against the authoritative server line list
|
||
(`req[].itm`, the co shape). The server may not only change a line's values but also
|
||
ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines
|
||
("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of
|
||
that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert
|
||
the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept
|
||
distinct from 0). Rows the server no longer lists are removed; order follows the server.
|
||
Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */
|
||
applyItems: function (tbl, req) {
|
||
let val = (x) => (x === undefined ? null : x); // JSON null stays null; absent -> null
|
||
(req || []).forEach((b) => {
|
||
if (!b) { return; }
|
||
let bid = (b.Id || '').toString(); if (bid === '') { return; }
|
||
let bdy = tbl.children('tbody').filter((i, e) => (($(e).data() || {}).Id || '').toString() === bid).first();
|
||
if (bdy.length < 1) { return; }
|
||
let itms = b.itm || [], serverIds = {};
|
||
let title = bdy.find('tr.title').first();
|
||
let prev = title.length ? title : null;
|
||
itms.forEach((co) => {
|
||
let id = (co.id || '').toString(); if (id === '') { return; }
|
||
serverIds[id] = true;
|
||
let rw = bdy.find('#itm' + id).first(), isNew = rw.length < 1;
|
||
if (isNew) { rw = $$.tr(bdy, { id: 'itm' + id, class: 'itm' }); }
|
||
let dta = rw.data() || {};
|
||
let changed = isNew || (dta.Type || '') !== (co.typ || '')
|
||
|| dta.net !== val(co.v) || dta.net_val !== val(co.vt) || dta.vat_val !== val(co.vv)
|
||
|| dta.svcnet_val !== val(co.vs) || dta.svcvat_val !== val(co.vsv)
|
||
|| (dta.position || '') !== (co.p != null ? co.p : '');
|
||
$.extend(dta, {
|
||
Id: id, Type: co.typ,
|
||
net: val(co.v), net_val: val(co.vt), vat_val: val(co.vv),
|
||
svcnet_val: val(co.vs), svcvat_val: val(co.vsv),
|
||
vat: co.vat, position: (co.p != null ? co.p : ''),
|
||
SetItmId: (co.SetItmId != null ? co.SetItmId : null)
|
||
});
|
||
if (isNew) { $.extend(dta, { htmltext: co.t, quantity: co.q }); } // text only for server-created rows (e.g. the set row)
|
||
rw.data(dta);
|
||
if (prev) { if (rw.prev()[0] !== prev[0]) { rw.insertAfter(prev); } } // keep server order, but don't churn already-ordered rows
|
||
else if (bdy.children().first()[0] !== rw[0]) { bdy.prepend(rw); }
|
||
prev = rw;
|
||
if (changed) { $inv.rrw.call(rw); }
|
||
});
|
||
bdy.find('tr.itm').each((i, tr) => {
|
||
let id = (($(tr).data() || {}).Id || '').toString();
|
||
if (id !== '' && !serverIds[id]) { $(tr).remove(); } // dropped server-side (block.setonly)
|
||
});
|
||
});
|
||
},
|
||
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
||
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
||
server says should show no price (set members in SetPrice/SetOnly mode), and emphasises
|
||
the set header line — so the online editor always shows exactly what the PDF will print,
|
||
without duplicating the pricing rules client-side. Rows not present in the map (no set in
|
||
that block, or standalone items) are left untouched. */
|
||
applySetDisplay: function (tbl, setDisplay) {
|
||
tbl.find('tr.itm').each((i, tr) => {
|
||
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
||
if (id === '' || !Object.prototype.hasOwnProperty.call(setDisplay, id)) { return; }
|
||
let flags = setDisplay[id] || {};
|
||
rw.tC('sethdr', bool(flags.h, false));
|
||
if (bool(flags.p, true) === false) {
|
||
rw.find('td.currency').text('');
|
||
}
|
||
});
|
||
},
|
||
/* Send one change to the server; the draftReady signal and this success both refresh.
|
||
Returns the underlying ajax promise so callers that must not race a follow-up delta
|
||
against this one (e.g. $inv.toSetPrice after a flush) can chain on it.
|
||
If the initial seeding (inv/dopen) is still in flight (no token yet, but a
|
||
'dseedpromise' is pending), the delta previously vanished silently — the user saw no
|
||
network activity at all when clicking a context-menu action right after opening the
|
||
editor. Now the delta waits for the seed to finish and retries against the freshly
|
||
issued token instead of being dropped. */
|
||
sync: function (delta) {
|
||
let t = $inv.d.token();
|
||
if (t === '') {
|
||
let seeding = $inv.d.tbl().data('dseedpromise');
|
||
if (seeding) { return $.when(seeding).then(() => $inv.d.sync(delta)); }
|
||
return $.when();
|
||
}
|
||
$inv.d.layout().aC('freeze');
|
||
return $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'); } }
|
||
});
|
||
},
|
||
/* The current section id sequence (used to detect a reorder that changes no block content). */
|
||
order: function () { return (($inv.d.tbl().data('bai')) || []).map((b) => (b.Id || '').toString()); },
|
||
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
||
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
||
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
||
block.order delta; the server reorders the cache, renumbers positions and pushes them back.
|
||
No early return on an empty token here: sync() itself waits for an in-flight seed
|
||
(dseedpromise) instead of silently dropping the delta, so edits made in the brief window
|
||
before seeding completes are not lost. */
|
||
syncChanged: function (tbl) {
|
||
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [], pending = [];
|
||
$.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); } });
|
||
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
||
tbl.data('dhashes', next).data('dorder', order);
|
||
changed.forEach((b) => pending.push($inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b })));
|
||
removed.forEach((id) => pending.push($inv.d.sync({ Target: 'block.remove', Ref: id })));
|
||
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
||
if (sameSet && prevOrder.join(',') !== order.join(',')) { pending.push($inv.d.sync({ Target: 'block.order', Value: order })); }
|
||
/* $.when(...pending) so a caller (e.g. toSetPrice) can wait until every flushed block
|
||
has actually reached the server before sending a further delta that depends on it —
|
||
otherwise a delayed block.replace carrying pre-conversion values can land after and
|
||
silently overwrite a just-applied server-side mutation (e.g. item.setprice). */
|
||
return $.when.apply($, pending).promise();
|
||
},
|
||
/* Map an inline recipient field to its delta target and send it. No token-guard here for the
|
||
same reason as syncChanged: sync() waits for an in-flight seed instead of dropping the edit. */
|
||
syncField: function (nme, val) {
|
||
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');
|
||
}
|
||
};
|
||
/* ── 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; }
|
||
/* See $inv.d.applyState — drop stale/out-of-order dstate responses so an older refresh
|
||
can never roll back a newer one's totals/validation. */
|
||
let curVer = tbl.data('rdver');
|
||
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||
return;
|
||
}
|
||
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
||
$inv.rd.footer(tbl, state.sums || {});
|
||
$inv.rd.notes(state.notes || {});
|
||
$inv.rd.validation(state.validation || []);
|
||
},
|
||
/* Renders the backend-authoritative intro/closing notice paragraphs (ADR: notice texts
|
||
are backend-authoritative) exactly as FuchsPdf.BuildReminderNotes emits them for the
|
||
PDF, so the online editor always shows precisely what will be printed. */
|
||
notes: function (notes) {
|
||
let frm = $inv.rd.layout(); if (frm.length < 1) { return; }
|
||
let rq = frm.find('.srq'); if (rq.length < 1) { return; }
|
||
rq.children('.dnote').remove();
|
||
let intro = (notes || {}).intro || [], first = rq.children().first();
|
||
intro.slice().reverse().forEach((t) => {
|
||
let p = $$[0]('p').aC('dnote intro').rwText(t);
|
||
if (first.length) { p.insertBefore(first); } else { p.appendTo(rq); }
|
||
});
|
||
((notes || {}).closing || []).forEach((t) => $$[0]('p').aC('dnote closing').rwText(t).appendTo(rq));
|
||
},
|
||
/* 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. Detail rows ($inv.rRemRw) have
|
||
6 columns (aux, invoiceid, date, amount, amount_payed, open); span the label over the
|
||
4 columns between aux and the open-amount column so the total lands in the same last
|
||
column as the per-invoice open amounts. */
|
||
footer: function (tbl, sums) {
|
||
let ft = tbl.children('tfoot').empty();
|
||
let tr = $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).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 });
|
||
if (o) { o.ft.rwText($rct.rq1); }
|
||
let mainfnc = () => {
|
||
$ocms.postXT({
|
||
url: $ocms.url('req/get'), timeout: 60, data: { id: data.id, mode: 'r' }, success: (response) => {
|
||
//console.debug(response);
|
||
response.admin = response.admin || {};
|
||
let lf = $fis.lf(true).aC('fix').rC('hd');
|
||
$fis.frm_edit().IN();
|
||
$inv.eM(true, true);
|
||
if ((response.requests || []).length < 1) {
|
||
lf.aC('fix').text($rct.nd);
|
||
} else {
|
||
|
||
$$.dc('lh', lf, $rct.mdl);
|
||
let iv = $$.d(), rq = $$.ul({ class: 'rql' }).data({ search: data.id, parent: response.admin.parent }).appendTo(lf), rqa = {}, cl = $rcol.req.lbl();
|
||
$.each(response.requests || [], function (ri, rx) {
|
||
let rli = $$.li({ class: 'cli rli' }).data($.extend({}, rx)).appendTo(rq);
|
||
let lihd = $$.dc('lihd', rli).addClass(rx.state);
|
||
if (booln(rx.open, false) === true) {
|
||
lihd.append($$.sc('cbox').click(() => {
|
||
rli.tC('checked');
|
||
iv.find('li').rC('checked');
|
||
if (rli.is('.checked') === true) {
|
||
$inv.rMn(rx.open);
|
||
} else {
|
||
$inv.eM(true);
|
||
}
|
||
}));
|
||
}
|
||
lihd.append([$$.sc('eid', rx.ExternalId), $$.sc('nme', rx.Name)]);
|
||
$$.dc('lidt', rli).append([$$.dc('rqs').append([$$.s(cl.State + ': '), $$.s($rct.sts[rx.State || '-'])]), $$.dc('ivn').append([$$.s(cl.InvoiceId + ': '), $$.s(rx.InvoiceId || '- -')]), $$.dc('wda').append([$$.s(cl.WorkDoneAt + ': '), $$.s(fdt(rx.WorkDoneAt, 'dd.MM.yyyy'))])]);
|
||
rqa[rx.Id] = rli;
|
||
});
|
||
if ((response.inv || []).length > 0) {
|
||
$$.dc('lh', lf, $rct.invs);
|
||
iv = $$.ul({ class: 'ivl' }).appendTo(lf); /* first assignment to iv is fake, just to prevent issues */
|
||
$.each(response.inv || [], (ii, ix) => {
|
||
let ili = $$.li({ class: 'cli ili' }).data($.extend({}, ix)).appendTo(iv);
|
||
let lihd = $$.dc('lihd', ili).addClass(ix.invstatus);
|
||
if (booln(ix.isFinal, true) === false) {
|
||
lihd.append($$.sc('cbox').click(() => {
|
||
if ((ix.Id || '') !== '') {
|
||
ili.tC('checked').siblings().rC('checked'); rq.find('li').rC('checked');
|
||
if (ili.is('.checked') === true) {
|
||
$inv.iMnr(ix);
|
||
} else {
|
||
$inv.eM(true); /* empty menu */
|
||
}
|
||
}
|
||
}));
|
||
} else if (['', 'dft'].indexOf(ix.invstatus) < 0) {
|
||
lihd.append($$.sc('dli').click(function () { $inv.disp(ix.Id, 'inv'); }));
|
||
}
|
||
lihd.append($$.sc('nme', ix.DocumentName || ix.Id));
|
||
$$.dc('lidt', ili).append([$$.dc('wda').append([$$.s(fdt(ix.DateCreated, 'dd.MM.yyyy'))]), $$.d().text($ict.iSt[ix.invstatus] || ix.invstatus)]);
|
||
});
|
||
}
|
||
|
||
}
|
||
}, complete: () => {
|
||
if (o) { o.c.trigger('modal_close'); }
|
||
}
|
||
});
|
||
};
|
||
$ocms.postXT({
|
||
url: $ocms.url('req/pget'), timeout: 90, data: { id: data.id }, success: (r1) => {
|
||
if (o) { o.ft.rwText($rct.rq2); }
|
||
mainfnc();
|
||
}, error: () => {
|
||
if (confirm($rct.rq1f)) {
|
||
if (o) { o.ft.rwText($rct.rq2); }
|
||
mainfnc();
|
||
} else {
|
||
if (o) { o.c.trigger('modal_close'); }
|
||
}
|
||
}
|
||
});
|
||
};
|
||
$inv.ccInv = function (ev) { //normale rechnung
|
||
let data = ev.data || {}, typ = data.typ || 'r';
|
||
let lf = $fis.lf(), rql = lf.children('ul.rql'), parent = rql.data('parent');
|
||
let selli = [];
|
||
rql.find('li.rli.checked').each(function () {
|
||
selli.push($(this).data('Id'));
|
||
});
|
||
if (selli.length < 1) { // abort if none selected
|
||
alert($rct.dnS);
|
||
return;
|
||
}else if (typ === 'i' && selli.length > 1) { // abort if intermediate invoice and several selected
|
||
alert($rct.dII);
|
||
return;
|
||
}
|
||
let edf = $fis.frm_edit(), frm = $$.dc('invoice_layout', edf).append($$.dc('btn sprev').click($inv.sprev));
|
||
let ww = $fis.cf().width() > (frm.width() + lf.width() + 20);
|
||
lf.tC('fix', ww).tC('hd', !ww);
|
||
$inv.eM(false, true);
|
||
let fr = $$.dc('rfrm').ldng(1);
|
||
let o = $ocms.dlg(fr, { width: 1000 });
|
||
o.ft.rwText($rct.rq2);
|
||
$ocms.postXT({
|
||
url: $ocms.url('req/iget'), timeout: 60, data: { id: parent, mode: 'ful', typ: typ, sel: selli.join(',') }, success: (response) => {
|
||
//console.debug(response);
|
||
|
||
let rq = $$.dc('srq', frm);
|
||
let rif = $$.tblset({ class: 'invi' }, rq);
|
||
rif.bdy.remove();
|
||
rif.ft = $$[0]('tfoot');
|
||
response.admin = response.admin || {};
|
||
response.admin.p13b = bool(response.admin.p13b || '', (((response.inv || {}).InvoiceOptions || '').split(',').includes('§13b') === true));
|
||
rif.tbl.data($.extend({ new: {}, sms: {}, itm: {} }, { admin: response.admin, companies: response.companies, locations: response.locations }));
|
||
let thr = $$.tr(rif.hd).aC('shd').append([$$.th().aC('aux')]); /* header row for items */
|
||
$rct.invHR.forEach(h => $$.th(thr, h));
|
||
rif.tbl.on('fds.inv', $inv.invSumUpdate);
|
||
$.each(response.requests || [], function (ri, rx) {
|
||
let srqid = rx.Id || 0;
|
||
if (srqid !== 0) {
|
||
let worknotes = $inv.worknotes(rx);
|
||
rx.text = response.admin.type === 'i' ? ($rct.req + (jine([rx.ExternalId, rx.Name], ': ').eine(' ', ''))) : (jine([jine([fdt(rx.WorkDoneAt, 'dd.MM.yy'), rx.ExternalId], ' - ' + $rct.req + ' '), worknotes.ne(rx.Name)], ': \n'));
|
||
let bdy = $$.tbody(rif.tbl).data($.extend({}, rx));
|
||
$inv.rendersrq.call(bdy);
|
||
}
|
||
});
|
||
|
||
/* add an empty */
|
||
let atr = $$.tr($$.tbody(rif.tbl), { class: 'placeholder' }).data({ net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, net: 0 });
|
||
$inv.rrw.call(atr);
|
||
|
||
rif.ft.appendTo(rif.tbl);
|
||
|
||
|
||
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));
|
||
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);
|
||
$$.dc('sndfrm', frm).append($$.dc('content').text(admin.sender));
|
||
|
||
let admtxt, admlbl;
|
||
if (admin.provisionend) {
|
||
admlbl = admin.provisionstart ? $rct.provP : $rct.provD;
|
||
admtxt = admin.provisionstart ? (fdt(admin.provisionstart, 'dd.MM.yyyy') + ' - ' + fdt(admin.provisionend, 'dd.MM.yyyy')) : fdt(admin.provisionend, 'dd.MM.yyyy');
|
||
}
|
||
fm('admfrm', admtxt, 'provisionperiod', admlbl, 1, null);
|
||
rif.tbl.data('new').CustomValues = admin.CustomValues || '';
|
||
$$.dc('inpfrm ctpfrm', frm).text(jObj(admin.CustomValues, 'contactName'));
|
||
//adm = $$.dc('admfrm', frm), admc = $$.dc('content', adm),
|
||
|
||
rif.tbl.children('tbody').each($inv.bdysort);
|
||
rif.tbl.trigger('fds.inv'); /* trigger calculations */
|
||
|
||
$inv.eM(false, true, $inv.emOpt(admin));
|
||
}, complete: () => {
|
||
o.c.trigger('modal_close');
|
||
}
|
||
});
|
||
};
|
||
$inv.ccStInv = function (ev) { // Stornorechnung
|
||
let data = ev.data || {};
|
||
let lf = $fis.lf(), oldinv = data.id;
|
||
|
||
let frm = $$.dc('invoice_layout', $fis.frm_edit()).append($$.dc('btn sprev').click($inv.sprev));
|
||
let ww = $fis.cf().width() > (frm.width() + lf.width() + 20);
|
||
lf.tC('fix', ww).tC('hd', !ww);
|
||
$inv.eM(false, true);
|
||
let fr = $$.dc('rfrm').ldng(1);
|
||
let o = $ocms.dlg(fr, { width: 1000 });
|
||
o.ft.rwText($ict.iq1);
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/pget'), timeout: 90, data: { id: data.id }, success: (response) => {
|
||
if (o) { o.ft.rwText($ict.iq2); }
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/icget'), timeout: 60, data: { id: oldinv }, success: (response) => {
|
||
//console.debug(response);
|
||
|
||
let rq = $$.dc('srq', frm);
|
||
let rif = $$.tblset({ class: 'invi' }, rq);
|
||
rif.bdy.remove();
|
||
rif.ft = $$[0]('tfoot');
|
||
response.admin = response.admin || {};
|
||
response.admin.p13b = bool(response.admin.p13b || '', (((response.inv || {}).InvoiceOptions || '').split(',').includes('§13b') === true));
|
||
rif.tbl.data($.extend({ new: {}, sms: {}, itm: {} }, { admin: response.admin, companies: response.companies, locations: response.locations }));
|
||
let thr = $$.tr(rif.hd).aC('shd').append([$$.th().aC('aux')]); /* header row for items */
|
||
$rct.invHR.forEach(h => $$.th(thr, h));
|
||
rif.tbl.on('fds.inv', $inv.invSumUpdate);
|
||
$.each(response.requests || [], function (ri, rx) {
|
||
let srqid = rx.Id || 0;
|
||
if (srqid !== 0) {
|
||
let worknotes = $inv.worknotes(rx);
|
||
rx.text = response.admin.type === 'i' ? ($rct.req + (jine([rx.ExternalId, rx.Name], ': ').eine(' ', ''))) : (jine([fdt(rx.WorkDoneAt, 'dd.MM.yy') + worknotes.ne(rx.Name)], ': '));
|
||
let bdy = $$.tbody(rif.tbl).data($.extend({}, rx));
|
||
$inv.rendersrq.call(bdy);
|
||
}
|
||
});
|
||
|
||
/* add an empty */
|
||
let atr = $$.tr($$.tbody(rif.tbl), { class: 'placeholder' }).data({ net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, net: 0 });
|
||
$inv.rrw.call(atr);
|
||
|
||
rif.ft.appendTo(rif.tbl);
|
||
|
||
|
||
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));
|
||
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);
|
||
$$.dc('sndfrm', frm).append($$.dc('content').text(admin.sender));
|
||
|
||
let admtxt, admlbl;
|
||
if (admin.provisionend) {
|
||
admlbl = admin.provisionstart ? $rct.provP : $rct.provD;
|
||
admtxt = admin.provisionstart ? (fdt(admin.provisionstart, 'dd.MM.yyyy') + ' - ' + fdt(admin.provisionend, 'dd.MM.yyyy')) : fdt(admin.provisionend, 'dd.MM.yyyy');
|
||
}
|
||
fm('admfrm', admtxt, 'provisionperiod', admlbl, 1, null);
|
||
rif.tbl.data('new').CustomValues = admin.CustomValues || '';
|
||
$$.dc('inpfrm ctpfrm', frm).text(jObj(admin.CustomValues, 'contactName'));
|
||
//adm = $$.dc('admfrm', frm), admc = $$.dc('content', adm),
|
||
|
||
rif.tbl.children('tbody').each($inv.bdysort);
|
||
rif.tbl.trigger('fds.inv'); /* trigger calculations */
|
||
|
||
}, complete: () => {
|
||
o.c.trigger('modal_close');
|
||
}
|
||
});
|
||
|
||
}, error: () => {
|
||
if (o) { o.c.trigger('modal_close'); }
|
||
}
|
||
});
|
||
};
|
||
$inv.clCntInv = function (ev) { // click-handler for invoice continuation
|
||
let lf = $fis.lf(false), selli = [];
|
||
lf.find('li.ili.checked').each(function () {
|
||
selli.push($(this).data('Id'));
|
||
});
|
||
if (selli.length === 1) {
|
||
//$('#contentframe').empty();
|
||
$inv.cntInv({ id: selli[0] });
|
||
}
|
||
};
|
||
$inv.cntInv = function (data) { //invoice continuation
|
||
data = data || {};
|
||
let lf = $fis.lf(false).rC('fix').aC('hd'), frm = $$.dc('invoice_layout', $fis.frm_edit()).append($$.dc('btn sprev').click($inv.sedit));
|
||
$inv.eM(false, true);
|
||
let fr = $$.dc('rfrm').ldng(1);
|
||
let o = $ocms.dlg(fr, { width: 1000 });
|
||
o.ft.rwText($rct.rq2);
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/get'), timeout: 60, data: { id: data.id }, success: (response) => {
|
||
//console.debug(response);
|
||
response.admin = response.admin || {};
|
||
let inv = response.inv || {};
|
||
let rq = $$.dc('srq', frm);
|
||
let rif = $$.tblset({ class: 'invi' }, rq);
|
||
rif.bdy.remove();
|
||
rif.ft = $$[0]('tfoot');
|
||
rif.tbl.data($.extend({ invid: inv.Id, new: {}, sms: { }, itm: {}, bai: [] }, response));
|
||
let thr = $$.tr(rif.hd).aC('shd').append([$$.th().aC('aux')]); /* header row for items */
|
||
$rct.invHR.forEach(h => $$.th(thr, h));
|
||
rif.tbl.on('fds.inv', $inv.invSumUpdate);
|
||
$.each(response.req || [], function (ri, rx) {
|
||
let bdy = $$.tbody(rif.tbl).data($.extend({}, rx));
|
||
$inv.rendersrq.call(bdy);
|
||
});
|
||
|
||
/* add an empty */
|
||
let atr = $$.tr($$.tbody(rif.tbl), { class: 'placeholder' }).data({ net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, net: 0 });
|
||
$inv.rrw.call(atr);
|
||
|
||
rif.ft.appendTo(rif.tbl);
|
||
|
||
|
||
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));
|
||
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);
|
||
$$.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);
|
||
rif.tbl.data('new').CustomValues = inv.CustomValues || '';
|
||
$$.dc('inpfrm ctpfrm', frm).text(jObj(inv.CustomValues, 'contactName'));
|
||
//adm = $$.dc('admfrm', frm), admc = $$.dc('content', adm),
|
||
|
||
rif.tbl.children('tbody').each($inv.bdysort);
|
||
rif.tbl.trigger('fds.inv'); /* trigger calculations */
|
||
$inv.eM(false, true, $inv.emOpt(response.admin));
|
||
}, complete: () => {
|
||
o.c.trigger('modal_close');
|
||
}
|
||
});
|
||
};
|
||
$inv.cSt = function (data) {
|
||
data = data || {};
|
||
let lf = $fis.lf(), frm = $$.dc('invoice_layout', $fis.frm_edit()).append($$.dc('btn sprev').click($inv.sedit));
|
||
let ww = $fis.cf().width() > (frm.width() + lf.width() + 20);
|
||
lf.tC('fix', ww).tC('hd', !ww);
|
||
$inv.eM(false, true);
|
||
let fr = $$.dc('rfrm').ldng(1);
|
||
let o = $ocms.dlg(fr, { width: 1000 });
|
||
o.ft.rwText($ict.iq1);
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/pget'), data: { id: data.id }, success: (response) => {
|
||
if (o) { o.ft.rwText($ict.iq2); }
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/storno'), data: { id: data.id, mode: data.mode }, success: (response) => {
|
||
//console.debug(response);
|
||
response.admin = response.admin || {};
|
||
response.admin.p13b = bool(response.admin.p13b || '', (((response.inv || {}).InvoiceOptions || '').split(',').includes('§13b') === true));
|
||
let inv = response.inv || {};
|
||
let rq = $$.dc('srq', frm);
|
||
let rif = $$.tblset({ class: 'invi' }, rq);
|
||
rif.bdy.remove();
|
||
rif.ft = $$[0]('tfoot');
|
||
rif.tbl.data($.extend({ invid: inv.Id, new: {}, sms: {}, itm: {}, bai: [] }, response));
|
||
let thr = $$.tr(rif.hd).aC('shd').append([$$.th().aC('aux')]); /* header row for items */
|
||
$rct.invHR.forEach(h => $$.th(thr, h));
|
||
rif.tbl.on('fds.inv', $inv.invSumUpdate);
|
||
$.each(response.req || [], function (ri, rx) {
|
||
let bdy = $$.tbody(rif.tbl).data($.extend({}, rx));
|
||
$inv.rendersrq.call(bdy);
|
||
});
|
||
|
||
/* add an empty */
|
||
let atr = $$.tr($$.tbody(rif.tbl), { class: 'placeholder' }).data({ net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, net: 0 });
|
||
$inv.rrw.call(atr);
|
||
|
||
rif.ft.appendTo(rif.tbl);
|
||
|
||
|
||
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));
|
||
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);
|
||
$$.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);
|
||
rif.tbl.data('new').CustomValues = inv.CustomValues || '';
|
||
$$.dc('inpfrm ctpfrm', frm).text(jObj(inv.CustomValues,'contactName'));
|
||
//adm = $$.dc('admfrm', frm), admc = $$.dc('content', adm),
|
||
|
||
rif.tbl.children('tbody').each($inv.bdysort);
|
||
rif.tbl.trigger('fds.inv'); /* trigger calculations */
|
||
|
||
}, complete: () => {
|
||
o.c.trigger('modal_close');
|
||
}
|
||
});
|
||
|
||
}, error: () => {
|
||
if (o) { o.c.trigger('modal_close'); }
|
||
}
|
||
});
|
||
};
|
||
$inv.eHtml = function (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 <p>
|
||
tags, which used to get posted and persisted verbatim (e.g. <p>18.06.2026</p> in the
|
||
Leistungsdatum). The backend sanitises HTML too (single source of truth, ADR 0006), but
|
||
keeping these plain here avoids the UI briefly holding the wrapped value. Multi-line fields
|
||
(invoiceaddress, loc) stay HTML-capable and are normalised to newlines server-side. */
|
||
let isPlainText = ['invoiceemail', 'provisionperiod', 'invoicetitle'].includes(ev.data.nme);
|
||
let flds = isPlainText
|
||
? [{ name: 'txt', label: 'Text', type: 'text', value: frmct.text() }]
|
||
: [{ name: 'txt', label: 'Text', type: 'html', value: frmct.html(), tinymce: true, attr: { style: 'height: 300px' } }];
|
||
let change = ev.data.change || null;
|
||
let sets = {
|
||
title: t.data('dialog') || '',
|
||
success: function (response) {
|
||
if (isPlainText) {
|
||
frmct.text(response.txt || '');
|
||
} else {
|
||
frmct.html(response.txt);
|
||
}
|
||
if (typeof change === 'function') {
|
||
change(response.txt);
|
||
}
|
||
/* 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 }
|
||
}
|
||
if (Array.isArray(ev.data.list)) {
|
||
let optf = $$.dc('lstfrm');
|
||
$.each(ev.data.list, (ii, lx) => {
|
||
let li = $$.dc('li', optf).append((ev.data.lbl || '') !== '' ? $$.dc('lbl').rwText(lx[ev.data.lbl]) : null);
|
||
$$.dc('adr', li).rwText(lx[ev.data.property]).data('val', lx[ev.data.property]).click(function () {
|
||
let t = $(this), tgt = t.closest('.modal-body').find(':input[name="txt"]');
|
||
if (tgt.is('.tinymce')) {
|
||
tinymce.get(tgt.attr('id')).setContent($$.s().rwText(t.data('val')).html());
|
||
} else if (tgt.prop('tagName') === 'TEXTAREA') {
|
||
tgt.val(t.data('val')).change();
|
||
} else {
|
||
tgt.rwText(t.data('val'));
|
||
}
|
||
});
|
||
});
|
||
sets.addcontent = optf;
|
||
}
|
||
$ocms.dlgform(flds, sets);
|
||
};
|
||
/* No client-side arithmetic (ADR 0006/0008): only the chosen VAT rate is stored on each row;
|
||
the server (InvoiceDraftCalculator.RecomputeLineValues) recomputes vat_val/svcvat_val from it. */
|
||
$inv.setVat = function (ev) {
|
||
let t = $(this), thisrow = ev.data;
|
||
let vat = prompt($rct.rqV);
|
||
if (vat) {
|
||
vat = parseFloat(vat.replace('%', ''));
|
||
if (vat > 1) {
|
||
vat = vat * 0.01;
|
||
}
|
||
if (isNaN(vat) === false) {
|
||
thisrow.siblings('.itm').each(function () {
|
||
let rwi = $(this), dta = rwi.data();
|
||
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
||
});
|
||
$inv.t_fds_inv();
|
||
}
|
||
}
|
||
};
|
||
$inv.inRow = function (ev) {
|
||
let t = $(this), thisrow = ev.data, dta = {}, flds = $rcol.itm.clone(['SortOrder', 'NameOrNumber', 'Type', 'quantityhours', 'UnitString', 'net', 'svcnet_val', 'svcvat_val', 'net_val', 'vat_val', 'vat', 'Note']); /* fields without values */
|
||
let nid = 'N' + (((1 + Math.random()) * 0x10000) || 0).toString(16).substr(6), newrow = $$.tr({ id: 'itm_' + nid.toString(), class: 'itm' });
|
||
$ocms.dlgform(flds, {
|
||
title: t.data('dialog') || '',
|
||
success: function (response) {
|
||
newrow.data($.extend({ Id: nid }, dta, response));
|
||
$inv.rrw.call(newrow);
|
||
newrow.insertAfter(thisrow);
|
||
$inv.t_fds_inv();
|
||
}, typedvalues: true
|
||
});
|
||
};
|
||
$inv.eRow = function (ev) {
|
||
let t = $(this), row = ev.data, dta = row.data() || {}, fnme = ['SortOrder', 'NameOrNumber', 'Type', 'quantityhours', 'UnitString', 'net', 'svcnet_val', 'svcvat_val', 'net_val', 'vat_val', 'vat', 'Note'];
|
||
if (!dta.id && (dta.Type || '') === '') {
|
||
fnme.unshift('Type');
|
||
}
|
||
let flds = $rcol.itm.clone(fnme).applyValues(dta);
|
||
flds.set('Type', 'hidden', 'type');
|
||
$inv.eRw.call(t, row, dta, flds);
|
||
};
|
||
$inv.eRw = function(row, dta, flds) {
|
||
let t = $(this);
|
||
$ocms.dlgform(flds, {
|
||
title: t.data('dialog') || '',
|
||
success: function (res) {
|
||
let d = {}
|
||
if ((dta.Id || '') === '') {
|
||
d.Id = 'N' + (((1 + Math.random()) * 0x10000) || 0).toString(16).substr(6);
|
||
row.attr('id', 'itm_' + d.Id.toString());
|
||
}
|
||
d.quantity = ((res.quantityhours ||'').toString() + ' ' + (res.UnitString || '').toString()).trimEnd();
|
||
row.data($.extend({}, dta, res, d));
|
||
console.debug('eRw success %o', row.data());
|
||
$inv.rrw.call(row);
|
||
$inv.t_fds_inv();
|
||
}, typedvalues: true
|
||
});
|
||
};
|
||
/* Reorder items via drag. The DOM swap happens inside the Sortable during the drag; we commit
|
||
the new order once, reliably, on drop (onend) — that recomputes positions/totals and pushes the
|
||
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(); } }) }
|
||
$inv.rrw = function () {
|
||
let rw = $(this), dta = rw.data(), co = {}, ph = rw.is('.placeholder'), hn = rw.is('.hidenote');
|
||
let oHtml = (e) => $$.d().append(e).html();
|
||
let bc = [
|
||
$$.dc('ibtn insb', { title: $rct.iRb }).append(gi('indent-left')).click(rw, $inv.inRow)
|
||
];
|
||
if (ph === false) {
|
||
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
||
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
||
/* "Auf Setpreis umstellen": offered on a Set header item that does not yet carry a
|
||
price. fds__prepInvoice's [SetItmId] now anchors on the still-unconverted (price 0)
|
||
header, so the header row's own SetItmId self-references its own id rather than being
|
||
null — the real signal for showing this button is still the header's own price/net_val
|
||
being 0 (once converted it carries a nonzero price and the button disappears).
|
||
Irreversible: the actual sum/clear mutation happens server-side (backend-authoritative,
|
||
see InvoiceDraftEditService.ApplyItemSetPrice) via the "item.setprice" patch target. */
|
||
if ((dta.Type || '').toLowerCase() === 'set' && (dta.net_val || 0) === 0) {
|
||
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
||
}
|
||
}
|
||
let axf = $$.dc('axf').append(bc);
|
||
if (ph === true) {
|
||
co = { id: '', typ: 'placeholder' };
|
||
} else if (rw.is('.itm.osum') === true) {
|
||
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
||
} else {
|
||
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false, SetItmId: dta.SetItmId || null };
|
||
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
||
|
||
co.p = dta.position || (dta.SortOrder || '');
|
||
if (co.id === '') {
|
||
co.t = '';
|
||
} else if (['Text', 'Title'].includes(co.typ) && (dta.net_val || 0) === 0) {
|
||
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
||
} else {
|
||
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
||
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */
|
||
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
||
co.v = dta.net
|
||
co.vt = dta.net_val
|
||
}
|
||
}
|
||
if ((dta.Note || '') !== '') {
|
||
$$.dc('ibtn add', axf).append(gi('object-align-left')).click(function (e) {
|
||
$inv.rrw.call(rw.tC('hidenote'));
|
||
});
|
||
}
|
||
let tda = [
|
||
$$.tdc('aux').append(axf),
|
||
$$.tdc('keep').text(co.p)
|
||
];
|
||
if (co.id === '') {
|
||
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
||
} else {
|
||
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
||
/* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis"
|
||
members, and the set row's own unit-price), distinct from a real 0,00 €. */
|
||
let cur = (x) => (x == null ? '' : fnum(x, $rct.cst));
|
||
Array.prototype.push.apply(tda, [
|
||
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
||
$$.tdc('currency').text(cur(co.v)),
|
||
$$.tdc('currency inetval').text(cur(co.vt)).attr('title', $rct.svcPart + ': ' + cur(co.vs))
|
||
]);
|
||
}
|
||
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ)
|
||
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */
|
||
.tC('hidenote', hn).append(tda);
|
||
dta.co = co;
|
||
};
|
||
/* "Auf Setpreis umstellen" (see $inv.rrw): backend-authoritative (ADR 0006) — the browser only
|
||
posts the target set-header item id; the server (InvoiceDraftEditService.ApplyItemSetPrice)
|
||
sums the member rows (same block, [SetItmId] equal to this row's id), writes that sum onto
|
||
the set header and clears each member's price, then the normal draftReady refresh re-renders
|
||
both the header and its members from the authoritative session state. This mirrors the PDF's
|
||
read of the same cached session, so the online editor and PDF can never drift apart. */
|
||
$inv.toSetPrice = function (ev) {
|
||
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
||
if (id === '') { return; }
|
||
if (confirm($rct.toSetPc) === false) { return; }
|
||
/* Wait for the flush of any pending local edits to actually reach the server before sending
|
||
the conversion delta — sending both in parallel let a delayed block.replace (still carrying
|
||
the pre-conversion, zero member/header values) land after item.setprice and silently
|
||
overwrite the just-converted totals back to 0. */
|
||
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: 'item.setprice', Ref: id }); });
|
||
};
|
||
/* No client-side totals/footer/tax computation (ADR 0006/0008): this only re-assembles each
|
||
block's row contract array (needed to post the "req" shape to the server) and, on the very
|
||
first pass, seeds the authoritative backend session. Footer, VAT breakdown, service-refund
|
||
note figures and the per-block sum cell are never computed here — they are rendered exclusively
|
||
from the server's `dstate` response by $inv.d.footer / $inv.d.applyState once the draft has a
|
||
token. Before a token exists the footer/notes/isum cells are simply left empty; there is no
|
||
local approximation to display. */
|
||
$inv.invSumUpdate = function () {
|
||
let tbl = $(this);
|
||
let ba = [];
|
||
let bds = tbl.children('tbody');
|
||
bds.each((bi, bdy) => {
|
||
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
||
b.tC('empty', itm.length < 1);
|
||
itm.each((ti, tx) => {
|
||
let rrx = $(tx).data() || {};
|
||
i.push(rrx.co);
|
||
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
||
Set grouping: fds__prepInvoice computes [SetItmId] per item, anchored on the still-unconverted
|
||
(price 0) Set header that owns it — this is the authoritative membership signal (not row order),
|
||
so items not tagged with a SetItmId by the server are never swept into a preceding set. The
|
||
header row's own SetItmId self-references its own id, but it is never its own member (excluded
|
||
by the id !== '' && !== citem.id check below). */
|
||
let citem = $inv.itemToContract(rrx);
|
||
let sid = (rrx.SetItmId || '').toString();
|
||
if (citem.type !== 'set' && sid !== '' && sid !== citem.id) { citem.setId = sid; }
|
||
citems.push(citem);
|
||
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
||
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
||
rrx.SortOrder = iso;
|
||
rrx.position = ipos;
|
||
$inv.rrw.call(tx);
|
||
}
|
||
});
|
||
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems });
|
||
});
|
||
let nonempty = tbl.find('tbody:not(.empty)').length;
|
||
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
||
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 = '';
|
||
rx.steps.forEach((s, si) => {
|
||
let flds;
|
||
try {
|
||
flds = JSON.parse(s.Data || {}).fields || [];
|
||
} catch (e) {
|
||
console.debug(e);
|
||
flds = [];
|
||
}
|
||
if (Array.isArray(flds || '') !== true) {
|
||
if (typeof flds === 'object' && Array.isArray(flds.field || '') === true) {
|
||
flds = flds.field;
|
||
} else {
|
||
flds = [];
|
||
}
|
||
}
|
||
flds.forEach((v, si) => {
|
||
if (v.name === 'Ausgeführte Arbeiten') {
|
||
wn = v.result || '';
|
||
}
|
||
});
|
||
});
|
||
return wn;
|
||
};
|
||
$inv.rendersrq = function () {
|
||
let bdy = $(this).empty(), onesum = bdy.is('.onesum');
|
||
let rx = bdy.data();
|
||
//if (ri > 0) {
|
||
// $$.tr(bdy).aC('sep nosort').append($$.td({ colspan: 6 }));
|
||
//}
|
||
let rtr = $$.tr(bdy, { id: 'srq' + rx.Id }).aC('title nosort'), cl = $rcol.itm.lbl();
|
||
let aux = $$.dc('axf').appendTo($$.tdc('aux', rtr));
|
||
$$.dc('ibtn osum', aux, { title: $rct.combP }).append(gi('euro')).click(function (e) {
|
||
bdy.tC('onesum');
|
||
$inv.rendersrq.call(bdy);
|
||
$inv.t_fds_inv();
|
||
});
|
||
$$.dc('ibtn setvat', aux, { title: $rct.sV }).append(gi('gbp')).click(rtr, $inv.setVat);
|
||
$$.dc('ibtn insb', aux, { title: $rct.iRb }).append(gi('indent-left')).click(rtr, $inv.inRow);
|
||
let stxt = $$.sc('text', rx.text);
|
||
let nme = $$.td(rtr, { colspan: onesum ? 4 : 5 }).append(stxt), oidta, numkeys = ['net_val', 'vat_val', 'svcnet_val', 'svcvat_val', 'net'];
|
||
$$.dc('ibtn edit', aux).data('dialog', $rcol.req.lbl().Name).append(gi('pencil')).click({ t: stxt, change: (nv) => { rx.text = nv; $inv.t_fds_inv(); } }, $inv.eHtml)
|
||
if (onesum) {
|
||
$$.tdc('currency isumval', rtr);
|
||
oidta = { Id: rx.Id.toString() + '_osum', net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, net: 0 };
|
||
oidta.tbl = $$.tblset({ class: 'stbl' });
|
||
}
|
||
$.each(rx.items || [], (ii, ix) => {
|
||
let itr, idta = { Id: ix.Id, net_val: ix.net_val || 0, vat_val: ix.vat_val || 0, svcnet_val: 0, svcvat_val: 0, net: ix.net || 0, Note: ix.Note || '' };
|
||
switch (ix.Type.toLowerCase()) {
|
||
case 'service':
|
||
idta.svcnet_val = ix.net_val || 0;
|
||
idta.svcvat_val = ix.vat_val || 0;
|
||
}
|
||
if (onesum) {
|
||
itr = $$.tr(oidta.tbl.bdy, { id: 'itm' + ix.Id, class: 'sitm' }).aC(ix.Type);
|
||
if (ix.Type === 'Text' || ix.Type === 'Title') {
|
||
$$.td(itr, { colspan: 2 }).html(ix.htmltext || ix.Note);
|
||
} else {
|
||
$$.tdc('keep', itr).text(ix.quantity || ((ix.quantityhours || 0) > 0 ? fnum(ix.quantityhours) + (ix.UnitString || '').eine(' ','') : ''));
|
||
if (ix.htmltext) {
|
||
$$.tdc('txt', itr).html(ix.htmltext);
|
||
} else {
|
||
$$.tdc('txt', itr).text(ix.NameOrNumber).attr('title', ix.Note);
|
||
}
|
||
}
|
||
$.each(numkeys, (k, ky) => { oidta[ky] += idta[ky]; });
|
||
itr.data(idta);
|
||
} else {
|
||
$.extend(idta, ix);
|
||
itr = $$.tr(bdy, { id: 'itm' + ix.Id, class: 'itm' });
|
||
itr.data(idta);
|
||
$inv.rrw.call(itr);
|
||
}
|
||
});
|
||
if (onesum) {
|
||
let oi = $$.tr(bdy, { id: 'itmsq' + rx.Id, class: 'itm osum' }).data(oidta);
|
||
$inv.rrw.call(oi);
|
||
} else {
|
||
let istr = $$.tr(bdy).aC('isum nosort');
|
||
$$.tdc('aux', istr);
|
||
$$.td(istr, { colspan: 4 }).text($rct.iSum);
|
||
$$.tdc('currency isumval', istr);
|
||
}
|
||
};
|
||
$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,
|
||
nor a 'dseedpromise', so they correctly fall through to the no-op). While the initial
|
||
seed (inv/dopen) is still in flight there is no token yet either, but a 'dseedpromise'
|
||
is pending — wait for it instead of skipping the flush, otherwise an edit made in that
|
||
brief window is silently never sent. Returns the flush promise so callers that must
|
||
sequence a follow-up server delta after this flush (e.g. $inv.toSetPrice) can wait for
|
||
it instead of racing it. */
|
||
if ((tbl.data('dtoken') || '') !== '') { return $inv.d.syncChanged(tbl); }
|
||
let seeding = tbl.data('dseedpromise');
|
||
return seeding ? $.when(seeding).then(() => $inv.d.syncChanged(tbl)) : $.when();
|
||
};
|
||
$inv.sedit = () => {
|
||
$inv.sprev(true);
|
||
};
|
||
$inv.jdisp = function (ev) {
|
||
ev.stopPropagation();
|
||
if (ev.data.id) {
|
||
$inv.disp(ev.data.id, (ev.data.typ ||''));
|
||
}
|
||
}
|
||
$inv.disp = (id, typ) => {
|
||
let u = '';
|
||
switch (typ) {
|
||
case 'inv':
|
||
u = 'inv/rdoc';
|
||
break;
|
||
case 'rem':
|
||
u = 'rem/rdoc';
|
||
break;
|
||
}
|
||
if (u !== '') {
|
||
$ocms.postXT({
|
||
url: $ocms.url(u), data: { id: id || '', typ: 'img' }, success: (response) => {
|
||
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = 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_min: 50, form: false, exclusive:false
|
||
});
|
||
}
|
||
});
|
||
}
|
||
};
|
||
$inv.jdbn = function (ev) {
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/rdocn'), data: { name: ev.data.id || '', typ: 'img' }, success: (response) => {
|
||
let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = 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
|
||
});
|
||
}
|
||
});
|
||
};
|
||
$inv.sp13b = () => {
|
||
var l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||
d.admin.p13b = true;
|
||
if ((d.inv.InvoiceOptions || '').split(',').includes('§13b') === false) {
|
||
d.inv.InvoiceOptions += ',§13b';
|
||
} 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,
|
||
total_net, vat }. Set membership (type:'set' header + setId on members) is added by
|
||
the caller. The invoice total comes from the registration balance, so per-item
|
||
totals here are purely presentational. */
|
||
$inv.itemToContract = function (rrx) {
|
||
rrx = rrx || {};
|
||
let oHtml = (e) => $$.d().append(e).html();
|
||
let type = (rrx.Type || '').toString().toLowerCase();
|
||
/* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so
|
||
block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */
|
||
let nz = (x) => (x == null ? null : x);
|
||
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' };
|
||
if (rrx.co && rrx.co.typ === 'osum') {
|
||
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
||
ci.desc = rrx.co.t || '';
|
||
ci.total_net = (rrx.net_val || 0);
|
||
} else if (['text', 'title'].includes(type) && (rrx.net_val || 0) === 0) {
|
||
/* heading / free-text line, no price */
|
||
ci.desc = rrx.htmltext || ((((rrx.NameOrNumber || '').substr(0, 1) !== '#') ? oHtml($$[0]('p').text(rrx.NameOrNumber || '')) : '') + (rrx.Note || ''));
|
||
ci.total_net = '';
|
||
} else {
|
||
/* normal priced item (incl. set headers, which carry their own set price or 0).
|
||
Mirrors the "itm"/co.t fallback above: when the row only carries pre-built
|
||
htmltext (no separate NameOrNumber/Note), use it as desc so the PDF isn't left
|
||
without a description for these rows. */
|
||
if (rrx.htmltext) {
|
||
ci.desc = rrx.htmltext;
|
||
} else {
|
||
ci.title = rrx.NameOrNumber || '';
|
||
ci.desc = rrx.Note || '';
|
||
}
|
||
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
||
ci.price_net = nz(rrx.net);
|
||
ci.total_net = nz(rrx.net_val);
|
||
}
|
||
return ci;
|
||
};
|
||
/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto
|
||
admin.setmode, which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token
|
||
the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was
|
||
always just the implicit pre-conversion default and is not a state the user can switch back to
|
||
once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice /
|
||
InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */
|
||
$inv.ssetmode = () => {
|
||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||
d.admin = d.admin || {};
|
||
let cur = (d.admin.setmode || 'setprice'), o;
|
||
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
||
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]);
|
||
o = $ocms.dlg(fr, { width: 800 });
|
||
};
|
||
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible,
|
||
backend-authoritative mutation grouped by service-request block. The server inserts a dedicated
|
||
set row per block (block sum) and either nulls each member's price (setprice) or removes the
|
||
members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The
|
||
browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace
|
||
can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */
|
||
$inv.setSetmode = (mode) => {
|
||
if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; }
|
||
let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice';
|
||
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: target }); });
|
||
};
|
||
$inv.sctp = () => {
|
||
let flds = $invcol.ctp;
|
||
$ocms.dlgform(flds, {
|
||
title: $ict.ctp,
|
||
success: function (response) {
|
||
|
||
var l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||
let cvo = {};
|
||
if (typeof d.new !== 'undefined' && (d.new.CustomValues || '').substr(0, 1) === '{') {
|
||
cvo = JSON.parse(d.inv.CustomValues);
|
||
}
|
||
cvo.contactName = response.name;
|
||
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
|
||
});
|
||
};
|
||
/* Normalises the editor's working model into the exact field names the C# backend
|
||
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload. No totals/VAT
|
||
are computed here (ADR 0006/0008: the online editor performs no arithmetic at all) — the
|
||
backend session computes total_net/total_gross/vat itself (InvoiceDraftCalculator) once
|
||
opened from this payload, and BuildFdsData/BuildInvoiceParams reads them from there:
|
||
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
||
new.paymentterm, admin.CustomerId -> admin.customerid.
|
||
Originals are kept alongside; the source objects are not mutated. */
|
||
$inv.invcPayload = function (d) {
|
||
d = d || {};
|
||
let nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
||
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
||
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
||
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
||
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
||
return { admin: adm, req: d.bai, new: nw };
|
||
};
|
||
/* 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();
|
||
$inv.cInv2({ id: s.search });
|
||
} catch (e) { }
|
||
};
|
||
/* No client-side arithmetic (ADR 0006/0008): quantity/price/VAT edits are posted as raw values
|
||
and the server (InvoiceDraftCalculator.RecomputeLineValues) computes net_val/vat_val/
|
||
svcnet_val/svcvat_val. The fields are left as entered here; the authoritative values are
|
||
pushed back onto the row by $inv.d.applyItems once the change reaches the backend. */
|
||
$inv.quantChange = function (i) { };
|
||
$inv.storno = function (id, fds) {
|
||
let o, fr = $$.dc('choicefrm').append([
|
||
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
||
, $$.dc('btn', 'Storno mit neuer Rechnung').click({ id: id }, (ev) => { o.c.trigger('modal_close'); $inv.ccStInv(ev); })
|
||
, $$.dc('btn', 'Storno mit best. Rechnung').tC('inactive', bool(fds, false) === false).click({ id: id, mode: 'copy' }, (ev) => { if (bool(fds, false) === true) { o.c.trigger('modal_close'); $inv.cSt(ev.data); }})
|
||
]);
|
||
o = $ocms.dlg(fr, { width: 1000 });
|
||
};
|
||
$inv.credit = function (id, fds) {
|
||
let o, fr = $$.dc('choicefrm').append([
|
||
$$.dc('btn', 'Gutschrift').click({ id: id, mode: 'credit' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
||
]);
|
||
o = $ocms.dlg(fr, { width: 1000 });
|
||
};
|
||
$inv.setPyd = function (id) { // set invoice payed
|
||
if (confirm($ict.cpyd)) {
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/setpyd'), timeout: 60, data: { id: id }, success: (response) => {
|
||
alert($ict.relm);
|
||
}, error: () => {
|
||
alert($t.f1);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
$inv.setUpd = function (id) { // set invoice payed
|
||
if (confirm($ict.cupd)) {
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/setupd'), timeout: 60, data: { id: id }, success: (response) => {
|
||
alert($ict.relm);
|
||
}, error: () => {
|
||
alert($t.f1);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
$inv.resendRem = function (ev) {
|
||
ev.stopPropagation();
|
||
if (ev.data.id && confirm(string($ict.remresc, [ev.data.name]))) {
|
||
$ocms.postXT({
|
||
url: $ocms.url('rem/resend'), timeout: 60, data: { id: ev.data.id }, success: (response) => {
|
||
alert(string($ict.remresr, [ev.data.name]));
|
||
}, error: () => {
|
||
alert($t.f1);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
$inv.dspRem = function (id) { //reminder creation
|
||
let fr = $$.dc('rfrm').ldng(1);
|
||
let o = $ocms.dlg(fr, { width: 1000 });
|
||
o.ft.rwText($rct.rq2);
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/getrem'), timeout: 60, data: { id: id, drafts: false }, success: (response) => {
|
||
o.ft.empty();
|
||
let ts = $$.tblset({ class: 'invtbl'}, fr.empty()), fd = $invcol.rem2;
|
||
let thr = $$.tr(ts.hd), haux = $$.th(thr);
|
||
$.each(fd.fields || [], (ci, cx) => {
|
||
$$.th(thr).text(cx.label);
|
||
});
|
||
let cst = false;
|
||
$.each(response, (ri, rw) => {
|
||
cst = !cst;
|
||
let tr = $$.tr(ts.bdy).tC('alt', cst), raux = $$.td(tr);
|
||
tr.click(function () {
|
||
tr.tC('selected').siblings().rC('selected');
|
||
});
|
||
if (bool(rw.hasFile, false)=== true) {
|
||
$$.dc('idl ilbtn', raux, { title: $ict.dl + '\n' + rw.DocumentName }).append(gi('save-file', 'ico')).click({ id: rw.Id }, $inv.downloadrem);
|
||
$$.dc('idl ilbtn', raux, { title: $ict.remdsp + '\n' + rw.DocumentName }).append(gi('eye-open', 'ico')).click({ id: rw.Id, typ: 'rem' }, $inv.jdisp);
|
||
$$.dc('idl ilbtn', raux, { title: $ict.remres + '\n' + rw.DocumentName }).append(gi('refresh', 'ico')).click({ id: rw.Id, typ: 'rem', name: rw.DocumentName }, $inv.resendRem);
|
||
}
|
||
$.each(fd.fields || [], (ci, cx) => {
|
||
let td = $$.td(tr).aC(cx.dtype), val = rw[cx.name];
|
||
if (typeof cx.dfnc === 'function') {
|
||
cx.dfnc.call(td, val, rw);
|
||
} else {
|
||
switch (cx.type || '') {
|
||
case 'date':
|
||
td.text(fdt(rw[cx.name], 'dd.MM.yy'));
|
||
break;
|
||
case 'datetime':
|
||
td.text(fdt(rw[cx.name]));
|
||
break;
|
||
case 'html':
|
||
td.append($$.dc('ctw').html(val));
|
||
td.append($$.dc('ttip').html(val));
|
||
break;
|
||
default:
|
||
td.text(rw[cx.name]);
|
||
}
|
||
}
|
||
switch (cx.name || '') {
|
||
case 'InvoiceId':
|
||
td.aC('keep');
|
||
break;
|
||
}
|
||
switch (typeof cx.title) {
|
||
case 'function':
|
||
cx.title.call(td, rw);
|
||
break;
|
||
case 'string':
|
||
td.attr('title', cs.title);
|
||
}
|
||
});
|
||
});
|
||
}, error: () => {
|
||
fr.empty();
|
||
o.ft.rwText($t.f1);
|
||
}, complete: () => {
|
||
fr.ldng(0);
|
||
}
|
||
});
|
||
};
|
||
$inv.ccRem = function (id, InvoiceId) { //reminder creation
|
||
let t = $(this);
|
||
$ocms.postXT({
|
||
url: $ocms.url('rem/lrem'), timeout: 60, data: { id: id }, success: (response) => {
|
||
let flds = $invcol.rid.clone(); /* fields without values */
|
||
flds.applyValues(response.ov);
|
||
let ac = $$.dc('ac'), tbl = $$.tblset({class: 'fullgrid fullwidth'}, ac);
|
||
if ((response.lst || []).length > 0) {
|
||
$$.d({ 'style': 'margin: 1.5rem 0 1rem 0;font-size: 110%;text-decoration: underline;'}).prependTo(ac).text($ict.rovlh);
|
||
let trh = $$.tr(tbl.hd);
|
||
$ict.rovl.forEach((f, n) => $$.th(trh, f));
|
||
$.each(response.lst, (ri, rw) => {
|
||
let tr = $$.tr(tbl.bdy);
|
||
tr.append([$$.tdc('keep', rw.subject), $$.tdc('currency', fnum(rw.amount, $rct.cst)), $$.tdc('currency', fnum(rw.amount_payed, $rct.cst)), $$.tdc('keep', fdt(rw.DateFinalized, 'dd.MM.yy'))]);
|
||
});
|
||
} else {
|
||
$$.td($$.tr(tbl.bdy), $ict.nd);
|
||
}
|
||
$ocms.dlgform(flds, {
|
||
addcontent: ac,
|
||
title: string($ict.remdt, [InvoiceId || '?']),
|
||
success: function (sets) {
|
||
$inv.ccRem_s2(id, sets);
|
||
}, typedvalues: true
|
||
});
|
||
}
|
||
});
|
||
};
|
||
$inv.rRemRw = function (dta) {
|
||
let tr = $(this), rem = dta.rm || {};
|
||
tr.empty().data({ invoiceid: rem.invoiceid, invoicedate: rem.invoicedate, amount: rem.amount, amount_payed: rem.amount_payed });
|
||
let axf = $$.dc('axf').append($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(tr, $inv.eRowR));
|
||
tr.append([$$.tdc('aux').append(axf), $$.tdc('keep', rem.invoiceid), $$.tdc('keep', fdt(rem.invoicedate, 'dd.MM.yy')), $$.tdc('currency', fnum(rem.amount, $rct.cst)), $$.tdc('currency', fnum(rem.amount_payed, $rct.cst)), $$.tdc('currency', fnum(rem.amount - rem.amount_payed, $rct.cst))]);
|
||
};
|
||
$inv.eRowR = function (ev) {
|
||
let t = $(this), row = ev.data, dta = row.data() || {};
|
||
let flds = $invcol.rem.clone().applyValues(dta);
|
||
$ocms.dlgform(flds, {
|
||
title: t.data('dialog') || '',
|
||
success: function (res) {
|
||
let tbl = t.closest('table'), tdta = tbl.data();
|
||
$.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
|
||
});
|
||
};
|
||
$inv.ccRem_s2 = function (id, sets) { //reminder creation
|
||
let lf = $fis.lf(false).rC('fix').aC('hd'), frm = $$.dc('invoice_layout', $fis.frm_edit()).append($$.dc('btn sprev').click($inv.rprev));
|
||
$inv.eM(false, true);
|
||
let fr = $$.dc('rfrm').ldng(1);
|
||
//let o = $ocms.dlg(fr, { width: 1000 });
|
||
//o.ft.rwText($rct.rq2);
|
||
$ocms.postXT({
|
||
url: $ocms.url('rem/get'), timeout: 60, data: $.extend({ id: id }, sets), success: (response) => {
|
||
//console.debug(response);
|
||
let rem = response.rm || {};
|
||
let rq = $$.dc('srq', frm);
|
||
/* Intro/closing notice paragraphs are backend-authoritative (see $inv.rd.notes,
|
||
populated from FuchsPdf.BuildReminderNotes via rem/dstate after seeding below) —
|
||
they are appended/prepended into this same .srq container once the draft session
|
||
is open, so no client-side wording is constructed here. */
|
||
let rif = $$.tblset({ class: 'invi' }, rq);
|
||
rif.ft = $$[0]('tfoot');
|
||
rif.tbl.data($.extend({ invid: rem.invid, new: {} }, response));
|
||
let thr = $$.tr(rif.hd).aC('shd').append([$$.th().aC('aux')]); /* header row for items */
|
||
$ict.remHR.forEach(h => $$.th(thr, h));
|
||
$inv.rRemRw.call($$.tr(rif.bdy), rif.tbl.data());
|
||
rif.ft.appendTo(rif.tbl);
|
||
|
||
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));
|
||
rif.tbl.data('new')[nme] = txt;
|
||
};
|
||
fm('tfrm', rem.subject, 'subject', 0, null);
|
||
fm('adrfrm', rem.invoiceaddress, 'invoiceaddress', 0, null);
|
||
// fm('locfrm', rem.ProvisionLocation, 'loc', 1, null);
|
||
fm('emailfrm', rem.invoiceemail, 'invoiceemail', 0, null);
|
||
$$.dc('sndfrm', frm).append($$.dc('content').text(rem.sender));
|
||
|
||
//adm = $$.dc('admfrm', frm), admc = $$.dc('content', adm),
|
||
|
||
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 = () => {
|
||
/* 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)) {
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/sis'), data: { id: id || '' }, success: (response) => { }
|
||
});
|
||
}
|
||
};
|
||
$inv.srs = (id) => {
|
||
if (confirm($ict.srsc)) {
|
||
$ocms.postXT({
|
||
url: $ocms.url('rem/srs'), data: { id: id || '' }, success: (response) => { }
|
||
});
|
||
}
|
||
};
|
||
|
||
$inv.mfrrel = (id) => {
|
||
$('#contentframe').ldng();
|
||
$ocms.postXT({
|
||
url: $ocms.url('inv/mfrrel'), data: { id: id || '' }, success: (response) => { $inv.rerenderinv(); }, complete: () => {
|
||
$('#contentframe').ldng(0);
|
||
}
|
||
});
|
||
};
|