let $spt = { mdl: 'Spezialfunktionen', intro: 'Versteckte Wartungsfunktionen für Administratoren. Jede Aktion wird protokolliert (fds__admin_activity / Anwendungslog).', run: 'Ausführen', working: 'Wird ausgeführt…', failed: 'Die Aktion ist fehlgeschlagen.', /* Card: invoice file backup + clear */ ifc_title: 'Rechnungsdatei zurücksetzen (Soft-Delete)', ifc_desc: 'Sichert die gespeicherte PDF-Datei einer finalisierten Rechnung (DB-Tabelle fds__invoice_file_backups + Kopie im Blob-Archiv) und entfernt sie aus der Rechnung. Beim nächsten Aufruf des Dokuments wird die PDF neu generiert (ADR 0025/0027).', ifc_id: 'Interne Rechnungs-ID', ifc_id_ph: 'z.B. W7VAL', ifc_id_note: 'Die interne ID (Spalte Id), nicht die Rechnungsnummer R2026-….', ifc_btn: 'Datei sichern und entfernen', ifc_confirm: 'Die Rechnungsdatei von "{0}" wird gesichert und aus der Rechnung entfernt. Fortfahren?', ifc_ok: 'Erledigt: Die Datei wurde gesichert und entfernt. Das Dokument wird beim nächsten Aufruf neu generiert.', ifc_ok_blob: 'Blob-Backup: {0}', ifc_ok_noblob: 'Kein Blob-Backup angelegt (Blob-Archiv deaktiviert, kein Blob vorhanden oder Kopie fehlgeschlagen — siehe Log).', ifc_reason: { invoice_not_found: 'Rechnung nicht gefunden.', not_final: 'Die Rechnung ist nicht finalisiert — nur finalisierte Rechnungen können zurückgesetzt werden (Entwürfe würden sonst dauerhaft als Entwurf gespeichert).', external: 'Externe (MFR-)Dokumente können nicht neu generiert werden.', no_file: 'Es ist keine Datei gespeichert — nichts zu tun.', db_error: 'Datenbankfehler — Details im Anwendungslog (CodeReference AdminBackupClearInvoiceFile).' } }; /* "Specialties" — admin-only page (URL /specialties, menu Administration → Spezialfunktionen) that exposes the otherwise hidden maintenance actions as cards with small forms. Each card calls its real action endpoint (which enforces the fds_sys > 4 gate itself); this module only renders and reports. Add a card per action in `cards`. */ let $spc = { init2: function () { $ocms.getScript([], function () { $spc.init3(); }); }, init3: function () { $fis.cf(true); $fis.lf(true); $('#activemodule').text($spt.mdl); $('#topbar').ocmsmenu([]); let frm = $$.dc('spcfrm', $('#contentframe')); $$.dc('spc_intro', frm).text($spt.intro); let grid = $$.dc('spc_grid', frm); $.each($spc.cards, function (i, build) { build(grid); }); }, /* ── building blocks ─────────────────────────────────────────────────── */ card: function (grid, title, desc) { let card = $$.dc('spc_card', grid); let hd = $$.dc('spc_card_hd', card); $$.s(title).appendTo(hd); let bd = $$.dc('spc_card_bd', card); if (desc) { $$.dc('spc_card_desc', bd).text(desc); } let form = $('
', { class: 'spc_form', novalidate: 'novalidate' }).appendTo(bd); let result = $$.dc('spc_result', bd).hide(); return { card: card, body: bd, form: form, result: result }; }, field: function (form, name, label, options) { options = options || {}; let fg = $$.dc('spc_field', form); let id = 'spc_' + name; $$.lbl(label, { for: id }).appendTo(fg); let inp = $$.i({ id: id, name: name, type: options.type || 'text', placeholder: options.placeholder || '' }).appendTo(fg); if (options.required) { inp.attr('required', 'required'); } if (options.pattern) { inp.attr('pattern', options.pattern); } if (options.note) { $$.dc('spc_field_note', fg).text(options.note); } return inp; }, showResult: function (result, ok, lines) { result.empty().attr('class', 'spc_result ' + (ok ? 'spc_result_ok' : 'spc_result_err')); $.each([].concat(lines || []), function (i, l) { if (l) { $$.dc('spc_result_line', result).text(l); } }); result.show(); }, /* Wires a card form: validates, confirms, POSTs, reports. `run(values)` must return a postXT options object without success/error/complete; `report(response)` returns { ok, lines }. Callbacks run inside jQuery's dispatch and must never throw. */ submit: function (c, options) { let btn = $$.bbtn(options.button, 'spc_btn spc_btn_primary').attr('type', 'submit').appendTo($$.dc('spc_actions', c.form)); c.form.on('submit', function (ev) { ev.preventDefault(); let f = c.form[0]; if (typeof f.reportValidity === 'function' && !f.reportValidity()) { return; } let values = c.form.serializeObject(true, { typedvalues: false }); if (typeof options.confirm === 'function' && !confirm(options.confirm(values))) { return; } c.result.hide(); btn.prop('disabled', true).text($spt.working); $ocms.postXT($.extend(options.run(values), { success: function (r) { let rep = options.report(r || {}); $spc.showResult(c.result, rep.ok, rep.lines); }, error: function (jqXHR) { $spc.showResult(c.result, false, [$spt.failed, (jqXHR && jqXHR.internalText) || '']); }, complete: function () { btn.prop('disabled', false).text(options.button); } })); }); }, /* ── cards ───────────────────────────────────────────────────────────── */ cards: [ /* Invoice file backup + clear → POST /do/admin/backupclearinvoicefile/