Verbesserung Ablagedialog bei mehreren ausgewählten Dateien
This commit is contained in:
357
ui/schritt.js
Normal file
357
ui/schritt.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
Reference in New Issue
Block a user