Anpassungen für ATN-Listing

This commit is contained in:
2026-09-21 21:44:35 +02:00
parent 8380dc1584
commit 2e9d62cf95
26 changed files with 207 additions and 598 deletions

View File

@@ -1,54 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title data-i18n="classifyTitle">Klassifizierung bearbeiten</title>
<link rel="stylesheet" href="gemeinsam.css">
</head>
<body>
<h1 data-i18n="classifyTitle">Klassifizierung bearbeiten</h1>
<div id="quelle" class="hinweis"></div>
<label for="titel" data-i18n="fieldTitle">Titel</label>
<input type="text" id="titel">
<label for="erstellt" data-i18n="fieldCreated">Erstellt</label>
<input type="text" id="erstellt" placeholder="JJJJ-MM-TT">
<label for="korr" data-i18n="fieldCorrespondent">Korrespondent</label>
<input type="text" id="korr" list="korrListe" autocomplete="off">
<datalist id="korrListe"></datalist>
<label for="typ" data-i18n="fieldDocType">Dokumenttyp</label>
<input type="text" id="typ" list="typListe" autocomplete="off">
<datalist id="typListe"></datalist>
<div class="wachsen">
<label for="tagSuche" data-i18n="fieldTags">Tags</label>
<input type="text" id="tagSuche"
data-i18n-placeholder="tagsFilterPlaceholder"
placeholder="Tippen zum Filtern …">
<div class="liste">
<div id="tagsGewaehlt" class="gewaehlteTags"></div>
<div id="tags" style="padding:4px 8px"></div>
<div id="tagsLeer" class="hinweis"
style="padding:6px 8px; display:none"
data-i18n="tagsNoMatch">Keine Übereinstimmung.</div>
</div>
</div>
<div class="hinweis" data-i18n="classifyHintNew">
Unbekannte Korrespondenten und Dokumenttypen werden angelegt.
</div>
<div id="meldung" class="hinweis"></div>
<div class="fuss">
<button id="abbrechenAlle" data-i18n="btnSkipAll">Alle überspringen</button>
<button id="ueberspringen" data-i18n="btnSkip">Überspringen</button>
<button id="speichern" class="haupt" data-i18n="btnSave">Speichern</button>
</div>
<script type="module" src="classify.js"></script>
</body>
</html>

View File

@@ -1,262 +0,0 @@
/**
* classify.js - Klassifizierung eines gerade abgelegten Dokuments.
*
* Wird unmittelbar nach dem Ablegen aufgerufen. Ohne diesen Schritt trägt
* ein neues Dokument nur den Titel, und nachträglich macht man es
* erfahrungsgemäß nicht mehr.
*/
import { kurz } from "../lib/settings.js";
import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen }
from "./gemeinsam.js";
const $ = (id) => document.getElementById(id);
let dokumentId = null;
let alleTags = [];
// Bei mehreren abgelegten Dateien wird jede einzeln klassifiziert.
// Andernfalls trüge nur die erste eine sinnvolle Zuordnung, und die
// übrigen fielen später als unklassifiziert auf - wenn überhaupt.
let liste = [];
let stelle = 0;
// Gewählte Tags als Menge. Bewusst getrennt vom angezeigten Zustand der
// Kontrollkästchen: Der Filter blendet Zeilen aus, die Auswahl darf davon
// nicht berührt werden.
const gewaehlt = new Set();
document.addEventListener("DOMContentLoaded", start);
async function start() {
uebersetzeSeite();
const p = new URLSearchParams(location.search);
// "ids" für mehrere, "id" für den Einzelfall - letzteres bleibt für
// Aufrufe aus anderen Stellen erhalten.
const roh = p.get("ids") || p.get("id") || "";
liste = roh.split(",").map((x) => Number(x)).filter(Boolean);
stelle = 0;
$("ueberspringen").addEventListener("click", weiter);
$("abbrechenAlle").addEventListener("click", () => window.close());
$("speichern").addEventListener("click", speichern);
if (!liste.length) {
fehlerAnzeigen("Kein Dokument angegeben.");
return;
}
try {
const stamm = await ruf("stammdaten");
alleTags = stamm.tags;
fuelleListe("korrListe", stamm.korr);
fuelleListe("typListe", stamm.typen);
zustand.korr = stamm.korr;
zustand.typen = stamm.typen;
} catch (err) {
fehlerAnzeigen(err.message);
return;
}
await anzeigen();
}
const zustand = { korr: [], typen: [] };
/** Das Dokument an der aktuellen Stelle laden und darstellen. */
async function anzeigen() {
dokumentId = liste[stelle];
$("speichern").disabled = false;
$("ueberspringen").disabled = false;
meldungAnzeigen("");
// Der Knopf zum Abbrechen aller weiteren ist nur sinnvoll, wenn noch
// welche folgen.
$("abbrechenAlle").hidden = liste.length < 2;
try {
const doc = await ruf("dokument", { id: dokumentId });
const zaehler = liste.length > 1
? ` · ${browser.i18n.getMessage("classifyProgress",
[String(stelle + 1), String(liste.length)])
|| `${stelle + 1} von ${liste.length}`}`
: "";
$("quelle").textContent = `#${dokumentId} · ${kurz(doc.title, 60)}`
+ zaehler;
$("titel").value = doc.title || "";
$("erstellt").value = (doc.created || "").slice(0, 10);
$("korr").value = nameVon(zustand.korr, doc.correspondent);
$("typ").value = nameVon(zustand.typen, doc.document_type);
zeigeTags(doc.tags || []);
if ($("tagSuche")) $("tagSuche").value = "";
} catch (err) {
fehlerAnzeigen(err.message);
}
}
/** Zum nächsten Dokument, oder schliessen. */
async function weiter() {
stelle += 1;
if (stelle >= liste.length) {
window.close();
return;
}
await anzeigen();
}
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(liste, id) {
return liste.find((x) => x.id === id)?.name || "";
}
function zeigeTags(gesetzt) {
for (const id of gesetzt) gewaehlt.add(Number(id));
const feld = $("tagSuche");
if (feld) {
feld.addEventListener("input", zeichneTags);
// Eingabetaste soll den Dialog nicht abschicken, sondern beim
// einzigen Treffer diesen umschalten - das ist beim Tippen der
// erwartete Ablauf.
feld.addEventListener("keydown", (e) => {
if (e.key !== "Enter") return;
e.preventDefault();
const treffer = gefilterteTags();
if (treffer.length === 1) {
umschalten(treffer[0].id);
feld.value = "";
zeichneTags();
}
});
}
zeichneTags();
}
/** Tags, die zum aktuellen Filter passen und noch nicht gewählt sind. */
function gefilterteTags() {
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
return alleTags.filter((t) => {
if (gewaehlt.has(Number(t.id))) return false;
if (!filter) return true;
return String(t.name).toLowerCase().includes(filter);
});
}
function umschalten(id) {
const n = Number(id);
if (gewaehlt.has(n)) gewaehlt.delete(n);
else gewaehlt.add(n);
}
/**
* Tagliste zeichnen.
*
* Zwei Bereiche: oben die gewählten, darunter die übrigen. Die gewählten
* bleiben immer sichtbar, auch wenn der Filter sie ausschliesst - sonst
* verliert man beim Suchen den Überblick, was bereits angehakt ist.
*/
function zeichneTags() {
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
const oben = $("tagsGewaehlt");
oben.replaceChildren();
const gewaehlteListe = alleTags
.filter((t) => gewaehlt.has(Number(t.id)))
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
for (const tag of gewaehlteListe) {
oben.append(tagZeile(tag, true, ""));
}
const unten = $("tags");
unten.replaceChildren();
const rest = gefilterteTags()
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
for (const tag of rest) {
unten.append(tagZeile(tag, false, filter));
}
const leer = $("tagsLeer");
if (leer) leer.style.display = (rest.length === 0 && filter) ? "" : "none";
}
function tagZeile(tag, istGewaehlt, filter) {
const zeile = document.createElement("div");
zeile.className = "wahl";
const box = document.createElement("input");
box.type = "checkbox";
box.id = `t${tag.id}`;
box.value = String(tag.id);
box.checked = istGewaehlt;
box.addEventListener("change", () => {
umschalten(tag.id);
zeichneTags();
});
const bez = document.createElement("label");
bez.htmlFor = box.id;
beschriftungSetzen(bez, String(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.
*/
function beschriftungSetzen(el, name, filter) {
el.replaceChildren();
if (!filter) {
el.textContent = name;
return;
}
const pos = name.toLowerCase().indexOf(filter);
if (pos < 0) {
el.textContent = name;
return;
}
el.append(document.createTextNode(name.slice(0, pos)));
const treffer = document.createElement("span");
treffer.className = "trefferHervor";
treffer.textContent = name.slice(pos, pos + filter.length);
el.append(treffer);
el.append(document.createTextNode(name.slice(pos + filter.length)));
}
async function speichern() {
$("speichern").disabled = true;
meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …");
try {
// Aus der Menge lesen, nicht aus den Kästchen: Gefilterte Zeilen
// sind gar nicht im Dokument, ihre Auswahl ginge sonst verloren.
const tags = [...gewaehlt];
const payload = { title: $("titel").value.trim(), tags };
const erstellt = $("erstellt").value.trim();
if (erstellt) payload.created = erstellt;
await ruf("klassifizieren", {
id: dokumentId,
payload,
korrName: $("korr").value.trim(),
typName: $("typ").value.trim()
});
meldungAnzeigen(browser.i18n.getMessage("msgSaved") || "Gespeichert.");
setTimeout(weiter, 600);
} catch (err) {
fehlerAnzeigen(err.message);
$("speichern").disabled = false;
}
}

View File

@@ -29,7 +29,7 @@
<div class="wachsen">
<label data-i18n="colVersions">Versionen</label>
<div class="liste" style="min-height:70px">
<div class="liste" >
<table>
<thead><tr>
<th class="zahl" data-i18n="colNo">Nr</th>
@@ -68,6 +68,7 @@
</div>
<div class="hinweis" data-i18n="insertHintLink">
Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt.
Leeres Tag-Feld bedeutet: keine Kennzeichnung setzen.
</div>
</fieldset>