/** * schritt.js - Ziel und Klassifizierung, Datei für Datei. * * Der Ablauf ist bewusst in zwei Fenster geteilt: * * 1. Auswahl der abzulegenden Dateien (store.html) * 2. Je Datei: Ziel und Klassifizierung (dieses Fenster) * * Der Grund: Ziel und Klassifizierung sind je Datei verschieden. Ein * einzelner Dialog für alle würde die Angaben der ersten Datei auf alle * übertragen - bei einer Rechnung samt Lastschriftmandat wäre das falsch. * * Beim Anhängen an ein bestehendes Dokument wird dessen Klassifizierung * übernommen und kann angepasst werden. Sie neu einzugeben wäre Arbeit * für nichts, und ein Tippfehler brächte die Version vom Original weg. */ import { kurz, groesse } from "../lib/settings.js"; import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen } from "./gemeinsam.js"; const $ = (id) => document.getElementById(id); let auftrag = null; let liste = []; // die abzulegenden Dateien let stelle = 0; let stamm = { tags: [], korr: [], typen: [] }; let treffer = []; let zielDokument = null; let gewaehlteTags = new Set(); const abgelegt = []; // für die Abschlussmeldung document.addEventListener("DOMContentLoaded", start); async function start() { uebersetzeSeite(); const nummer = Number(new URLSearchParams(location.search).get("auftrag")); try { auftrag = await ruf("auftrag", { nummer }); if (!auftrag) throw new Error("Der Auftrag ist nicht mehr vorhanden."); liste = await ruf("dateiListe", { auftrag }); stamm = await ruf("stammdaten"); } catch (err) { fehlerAnzeigen(err.message); return; } if (!liste.length) { fehlerAnzeigen(browser.i18n.getMessage("errNothingSelected") || "Nichts ausgewählt."); return; } fuelleListe("korrListe", stamm.korr); fuelleListe("typListe", stamm.typen); verdrahten(); await anzeigen(); } function verdrahten() { for (const r of document.querySelectorAll('input[name=ziel]')) { r.addEventListener("change", zielGewechselt); } $("suchen").addEventListener("click", suchen); $("suche").addEventListener("keydown", (e) => { if (e.key === "Enter") suchen(); }); $("tagSuche").addEventListener("input", zeichneTags); $("abbrechen").addEventListener("click", () => window.close()); $("ueberspringen").addEventListener("click", () => weiter()); $("ablegen").addEventListener("click", ablegen); } // ------------------------------------------------------ Datei anzeigen async function anzeigen() { const eintrag = liste[stelle]; $("kopf").textContent = browser.i18n.getMessage("stepTitle") || "In Paperless ablegen"; const zaehler = liste.length > 1 ? `${browser.i18n.getMessage("classifyProgress", [String(stelle + 1), String(liste.length)]) || `${stelle + 1} von ${liste.length}`} · ` : ""; const name = eintrag.art === "nachricht" ? (browser.i18n.getMessage("stepWholeMessage") || "Die ganze Nachricht") : `${eintrag.name}${eintrag.groesse ? ` (${groesse(eintrag.groesse)})` : ""}`; $("quelle").textContent = `${zaehler}${name}`; // Zurücksetzen für die neue Datei zielDokument = null; treffer = []; $("treffer").replaceChildren(); $("suche").value = ""; $("tagSuche").value = ""; $("zielNeu").checked = true; gewaehlteTags = new Set(); $("titel").value = vorschlagTitel(eintrag); $("erstellt").value = auftrag.datum || ""; $("korr").value = ""; $("typ").value = ""; zielGewechselt(); zeichneTags(); meldungAnzeigen(""); $("ablegen").disabled = false; $("ueberspringen").hidden = liste.length < 2; } function vorschlagTitel(eintrag) { if (eintrag.art === "nachricht") return auftrag.titelVorschlag || ""; // Endung weg: Sie steht schon im Dateinamen des Originals und trägt // im Titel nichts bei. return String(eintrag.name || "").replace(/\.[^.]{1,8}$/, ""); } // ----------------------------------------------------------- Zielwahl function zielGewechselt() { const vorhanden = $("zielVorhanden").checked; $("suche").disabled = !vorhanden; $("suchen").disabled = !vorhanden; // Die Trefferliste abblenden statt ausblenden: Ein Feld, das // verschwindet und wieder auftaucht, lässt die Spalte springen. const liste = $("treffer").closest(".liste"); if (liste) liste.style.opacity = vorhanden ? "1" : ".45"; if (vorhanden && !treffer.length) suchen(); pruefen(); } async function suchen() { $("treffer").replaceChildren(); zielDokument = null; try { treffer = await ruf("suchen", { query: $("suche").value.trim() }); } catch (err) { fehlerAnzeigen(err.message); return; } for (const [i, r] of treffer.entries()) { const tr = document.createElement("tr"); const a = document.createElement("td"); a.className = "zahl"; a.textContent = r.archive_serial_number ?? "—"; const b = document.createElement("td"); b.textContent = (r.created || "").slice(0, 10); const c = document.createElement("td"); c.textContent = r.title || ""; c.title = r.title || ""; tr.append(a, b, c); tr.addEventListener("click", () => waehleZiel(i)); $("treffer").append(tr); } if (!treffer.length) { meldungAnzeigen(browser.i18n.getMessage("msgNoHits") || "Keine Treffer."); } pruefen(); } /** * Zieldokument wählen und dessen Klassifizierung übernehmen. * * Eine neue Version gehört inhaltlich zum selben Vorgang wie das * Original. Die Angaben neu einzutippen wäre Arbeit für nichts - und ein * abweichender Korrespondent brächte die Version vom Original weg. */ async function waehleZiel(i) { zielDokument = treffer[i] || null; for (const [j, tr] of [...$("treffer").children].entries()) { tr.classList.toggle("gewaehlt", j === i); } pruefen(); if (!zielDokument) return; meldungAnzeigen(browser.i18n.getMessage("msgLoadingTarget") || "Klassifizierung des Ziels wird geladen …"); try { const doc = await ruf("dokument", { id: zielDokument.id }); $("titel").value = doc.title || ""; $("erstellt").value = (doc.created || "").slice(0, 10); $("korr").value = nameVon(stamm.korr, doc.correspondent); $("typ").value = nameVon(stamm.typen, doc.document_type); gewaehlteTags = new Set((doc.tags || []).map(Number)); zeichneTags(); meldungAnzeigen(""); } catch (err) { fehlerAnzeigen(err.message); } } function pruefen() { const ok = $("zielNeu").checked || Boolean(zielDokument); $("ablegen").disabled = !ok; } // --------------------------------------------------------------- Tags function fuelleListe(id, eintraege) { const dl = $(id); dl.replaceChildren(); for (const e of eintraege) { const o = document.createElement("option"); o.value = e.name; dl.append(o); } } function nameVon(rows, id) { return rows.find((x) => x.id === id)?.name || ""; } /** * Tagliste zeichnen: Gewählte oben, dann die gefilterten übrigen. * * Gewählte bleiben sichtbar, auch wenn der Filter sie ausschliessen * würde - sonst verliert man beim Suchen den Überblick, was schon * angehakt ist. */ function zeichneTags() { const ziel = $("tags"); ziel.replaceChildren(); const filter = ($("tagSuche").value || "").trim().toLowerCase(); const oben = stamm.tags.filter((t) => gewaehlteTags.has(Number(t.id))); const unten = stamm.tags.filter( (t) => !gewaehlteTags.has(Number(t.id)) && (!filter || t.name.toLowerCase().includes(filter))); for (const t of oben) ziel.append(tagZeile(t, true, "")); if (oben.length && unten.length) { const tr = document.createElement("hr"); tr.style.cssText = "border:0;border-top:1px solid var(--rand);margin:5px 0"; ziel.append(tr); } for (const t of unten) ziel.append(tagZeile(t, false, filter)); if (!oben.length && !unten.length) { const h = document.createElement("div"); h.className = "hinweis"; h.textContent = browser.i18n.getMessage("tagsNoMatch") || "Keine Übereinstimmung."; ziel.append(h); } } function tagZeile(tag, gewaehlt, filter) { const zeile = document.createElement("div"); zeile.className = "wahl"; const box = document.createElement("input"); box.type = "checkbox"; box.id = `tag_${tag.id}`; box.checked = gewaehlt; box.addEventListener("change", () => { if (box.checked) gewaehlteTags.add(Number(tag.id)); else gewaehlteTags.delete(Number(tag.id)); zeichneTags(); }); const bez = document.createElement("label"); bez.htmlFor = box.id; if (gewaehlt) bez.style.fontWeight = "600"; beschriften(bez, tag.name, filter); zeile.append(box, bez); return zeile; } /** * Beschriftung mit hervorgehobenem Treffer. * * Aufbau über createElement und textContent, nicht über innerHTML: Ein * Tagname stammt aus Paperless und damit mittelbar aus fremden Quellen - * wer ein Dokument per Mail einliefern kann, bestimmt Tagnamen mit. */ function beschriften(el, name, filter) { el.replaceChildren(); const pos = filter ? name.toLowerCase().indexOf(filter) : -1; if (pos < 0) { el.textContent = name; return; } el.append(document.createTextNode(name.slice(0, pos))); const mark = document.createElement("span"); mark.className = "trefferHervor"; mark.textContent = name.slice(pos, pos + filter.length); el.append(mark, document.createTextNode(name.slice(pos + filter.length))); } // ------------------------------------------------------------- Ablauf async function ablegen() { $("ablegen").disabled = true; $("ueberspringen").disabled = true; meldungAnzeigen(browser.i18n.getMessage("msgUploading") || "Wird abgelegt …"); const eintrag = liste[stelle]; const anVorhandenes = $("zielVorhanden").checked; try { const ergebnis = await ruf("ablegenEine", { auftrag, eintrag, ziel: anVorhandenes ? "vorhanden" : "neu", zielId: zielDokument?.id, titel: $("titel").value.trim(), erstellt: $("erstellt").value.trim() }); // Klassifizierung anwenden. Beim Anhängen gilt sie für die Wurzel, // nicht für die neue Version. const zielId = ergebnis.art === "version" ? ergebnis.zielId : ergebnis.id; if (zielId) { meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Klassifizierung wird gespeichert …"); await ruf("klassifizieren", { id: zielId, payload: { title: $("titel").value.trim(), tags: [...gewaehlteTags], ...($("erstellt").value.trim() ? { created: $("erstellt").value.trim() } : {}) }, korrName: $("korr").value.trim(), typName: $("typ").value.trim() }); } abgelegt.push(ergebnis); await weiter(); } catch (err) { console.error("[paperless] Ablegen gescheitert", err); fehlerAnzeigen(err.message); $("ablegen").disabled = false; $("ueberspringen").disabled = false; } } async function weiter() { $("ueberspringen").disabled = false; stelle += 1; if (stelle >= liste.length) { meldungAnzeigen(browser.i18n.getMessage("msgStored", [String(abgelegt.length)]) || `${abgelegt.length} abgelegt.`); setTimeout(() => window.close(), 1200); return; } await anzeigen(); }