2026-06-03 11:15:59 +00:00
|
|
|
|
// Logik der Einstellungsseite.
|
|
|
|
|
|
|
|
|
|
|
|
const $ = (id) => document.getElementById(id);
|
2026-06-03 13:14:05 +00:00
|
|
|
|
// Passwort ist bewusst NICHT dabei – es wird nie gespeichert, nur zum Testen/Login
|
|
|
|
|
|
// transient verwendet (siehe "Verbindung testen").
|
|
|
|
|
|
const FIELDS = ["serverUrl", "organization", "username"];
|
2026-06-16 11:23:39 +00:00
|
|
|
|
const CHECKS = ["storeAttachments", "tagOnSuccess"];
|
2026-06-03 11:15:59 +00:00
|
|
|
|
|
|
|
|
|
|
function setStatus(msg, kind) {
|
|
|
|
|
|
const el = $("status");
|
|
|
|
|
|
el.textContent = msg;
|
|
|
|
|
|
el.className = kind || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 11:43:59 +00:00
|
|
|
|
// Warnt, wenn der Server über unverschlüsseltes http:// läuft – dann ginge das
|
|
|
|
|
|
// Passwort im Klartext über die Leitung. Liefert einen anzuhängenden Hinweis.
|
|
|
|
|
|
function insecureWarning(url) {
|
|
|
|
|
|
return /^http:\/\//i.test(String(url || "").trim())
|
|
|
|
|
|
? " ⚠ Achtung: http:// ist unverschlüsselt – bitte https:// verwenden."
|
|
|
|
|
|
: "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-20 14:16:35 +00:00
|
|
|
|
// Schnappschuss der auth-relevanten Felder (Server/Org/Benutzer) zum Zeitpunkt
|
|
|
|
|
|
// des letzten Speicherns/Ladens. Ändert sich davon etwas, ist ein evtl. noch im
|
|
|
|
|
|
// Broker liegender Token ungültig (er gehört zu den alten Daten) -> verwerfen.
|
|
|
|
|
|
let authSnapshot = { serverUrl: "", organization: "", username: "" };
|
|
|
|
|
|
const AUTH_FIELDS = ["serverUrl", "organization", "username"];
|
|
|
|
|
|
|
|
|
|
|
|
// Server-URLs nur für den Vergleich vereinheitlichen, damit ein rein kosmetischer
|
|
|
|
|
|
// Edit (Schrägstrich am Ende, Groß-/Kleinschreibung) nicht als Wechsel zählt.
|
|
|
|
|
|
const normUrl = (u) => String(u || "").trim().replace(/\/+$/, "").toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
function snapshotAuth(s) {
|
|
|
|
|
|
authSnapshot = { serverUrl: s.serverUrl || "", organization: s.organization || "", username: s.username || "" };
|
|
|
|
|
|
}
|
|
|
|
|
|
function authChanged(s) {
|
|
|
|
|
|
return AUTH_FIELDS.some((k) => (s[k] || "") !== (authSnapshot[k] || ""));
|
|
|
|
|
|
}
|
|
|
|
|
|
// Nur ein echter Server-Wechsel entwertet die Schrank-GUIDs (Schrankliste,
|
|
|
|
|
|
// Standardziel, gemerkte Ablagedialoge). Org-/Benutzer-Wechsel tut das nicht.
|
|
|
|
|
|
function serverChanged(s) {
|
|
|
|
|
|
return normUrl(s.serverUrl) !== normUrl(authSnapshot.serverUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Nach dem Speichern aufräumen, wenn sich Zugangsdaten geändert haben:
|
|
|
|
|
|
* Token immer verwerfen; bei Server-Wechsel zusätzlich alles wegwerfen, was
|
|
|
|
|
|
* Schrank-GUIDs des alten Servers enthält (sonst zeigt der Ablage-Dialog fremde
|
|
|
|
|
|
* Archive bzw. legt gegen eine nicht mehr existierende GUID ab).
|
|
|
|
|
|
* @returns {Promise<object>} die ggf. bereinigten Einstellungen
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function invalidateSession(s) {
|
|
|
|
|
|
if (!authChanged(s)) { snapshotAuth(s); return s; }
|
|
|
|
|
|
const server = serverChanged(s);
|
|
|
|
|
|
await browser.runtime
|
|
|
|
|
|
.sendMessage({ type: "auth:logout", scope: server ? "server" : "auth" })
|
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
|
let next = s;
|
|
|
|
|
|
if (server) {
|
|
|
|
|
|
next = await Settings.set({ defaultCabinetId: "", dialogByCabinet: {} });
|
2026-08-21 08:23:36 +00:00
|
|
|
|
// Gemerkte Feldlisten hängen an Schrank-GUIDs des alten Servers -> weg damit.
|
|
|
|
|
|
fieldsByCabinet = {};
|
|
|
|
|
|
await browser.storage.local.remove("fieldsByCabinet").catch(() => {});
|
|
|
|
|
|
availableFields = [];
|
2026-08-20 14:16:35 +00:00
|
|
|
|
fillCabinets([], ""); // Dropdowns leeren – die alte Liste gilt nicht mehr
|
2026-08-21 08:23:36 +00:00
|
|
|
|
refreshFieldSelects();
|
2026-08-20 14:16:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
snapshotAuth(next);
|
|
|
|
|
|
return next;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
async function load() {
|
|
|
|
|
|
const s = await Settings.get();
|
|
|
|
|
|
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
|
2026-08-20 14:16:35 +00:00
|
|
|
|
snapshotAuth(s);
|
2026-06-03 11:15:59 +00:00
|
|
|
|
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
|
2026-06-16 11:23:39 +00:00
|
|
|
|
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
|
2026-06-16 13:01:37 +00:00
|
|
|
|
$("mappingUrl").value = s.mappingUrl || "";
|
2026-08-21 08:23:36 +00:00
|
|
|
|
|
|
|
|
|
|
// Gemerkte Schrank- und Feldlisten VOR dem Aufbau der Zeilen lesen, damit die
|
|
|
|
|
|
// Feldname-Dropdowns schon beim ersten Rendern gefüllt sind.
|
|
|
|
|
|
const local = await browser.storage.local.get(["cabinets", "fieldsByCabinet"]);
|
|
|
|
|
|
fieldsByCabinet = local.fieldsByCabinet || {};
|
|
|
|
|
|
if (s.defaultCabinetId && Array.isArray(fieldsByCabinet[s.defaultCabinetId])) {
|
|
|
|
|
|
availableFields = fieldsByCabinet[s.defaultCabinetId];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 11:23:39 +00:00
|
|
|
|
loadMapping(s.fieldMapping && Object.keys(s.fieldMapping).length ? s.fieldMapping : DEFAULT_FIELD_MAP);
|
2026-08-21 08:23:36 +00:00
|
|
|
|
fillCabinets(local.cabinets || [], s.defaultCabinetId);
|
|
|
|
|
|
if (availableFields.length) {
|
|
|
|
|
|
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl (gemerkt).`;
|
|
|
|
|
|
}
|
2026-06-16 14:09:04 +00:00
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
// Best-effort auffrischen – nur wenn schon ein Sitzungs-Token da ist. Ohne
|
|
|
|
|
|
// Token bleibt es bei der gemerkten Liste, die Auswahl funktioniert trotzdem.
|
2026-06-16 14:09:04 +00:00
|
|
|
|
if (s.defaultCabinetId && [...$("mapCabinet").options].some((o) => o.value === s.defaultCabinetId)) {
|
|
|
|
|
|
$("mapCabinet").value = s.defaultCabinetId;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const stt = await browser.runtime.sendMessage({ type: "auth:status" });
|
|
|
|
|
|
if (stt && stt.hasToken && stt.token) {
|
|
|
|
|
|
Auth.setToken(stt.token);
|
|
|
|
|
|
availableFields = (await DocuWare.cabinetFieldNames(s, s.defaultCabinetId))
|
|
|
|
|
|
.filter((n) => n && !/^DW[A-Z]/.test(n)).sort();
|
2026-08-21 08:23:36 +00:00
|
|
|
|
await rememberFields(s.defaultCabinetId, availableFields);
|
2026-06-16 14:09:04 +00:00
|
|
|
|
refreshFieldSelects();
|
|
|
|
|
|
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
|
|
|
|
|
|
}
|
2026-08-21 08:23:36 +00:00
|
|
|
|
} catch (_) { /* egal – gemerkte Liste bleibt nutzbar */ }
|
2026-06-16 14:09:04 +00:00
|
|
|
|
}
|
2026-06-03 11:15:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 11:23:39 +00:00
|
|
|
|
function setMailFormat(v) {
|
|
|
|
|
|
const el = document.querySelector(`input[name="mailFormat"][value="${v}"]`);
|
|
|
|
|
|
if (el) el.checked = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
function getMailFormat() {
|
|
|
|
|
|
const el = document.querySelector('input[name="mailFormat"]:checked');
|
|
|
|
|
|
return el ? el.value : "eml";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 12:41:52 +00:00
|
|
|
|
function fillCabinets(cabinets, selectedId) {
|
|
|
|
|
|
// Standard-Ziel (Archive + Briefkörbe)
|
|
|
|
|
|
const sel = $("defaultCabinet");
|
|
|
|
|
|
sel.innerHTML = '<option value="">— keiner —</option>';
|
|
|
|
|
|
const addGroup = (label, list) => {
|
|
|
|
|
|
if (!list.length) return;
|
|
|
|
|
|
const g = document.createElement("optgroup");
|
|
|
|
|
|
g.label = label;
|
|
|
|
|
|
list.forEach((c) => {
|
|
|
|
|
|
const o = document.createElement("option");
|
|
|
|
|
|
o.value = c.id;
|
|
|
|
|
|
o.textContent = c.name;
|
|
|
|
|
|
if (String(c.id) === String(selectedId)) o.selected = true;
|
|
|
|
|
|
g.appendChild(o);
|
|
|
|
|
|
});
|
|
|
|
|
|
sel.appendChild(g);
|
|
|
|
|
|
};
|
|
|
|
|
|
addGroup("Archive", cabinets.filter((c) => !c.isBasket));
|
|
|
|
|
|
addGroup("Briefkörbe", cabinets.filter((c) => c.isBasket));
|
|
|
|
|
|
|
|
|
|
|
|
// Mapping-Helfer: nur Archive (Briefkörbe haben keine Indexfelder).
|
|
|
|
|
|
const mc = $("mapCabinet");
|
|
|
|
|
|
const keep = mc.value;
|
|
|
|
|
|
mc.innerHTML = '<option value="">Feldnamen aus Archiv vorschlagen …</option>';
|
|
|
|
|
|
cabinets.filter((c) => !c.isBasket).forEach((c) => {
|
|
|
|
|
|
const o = document.createElement("option");
|
|
|
|
|
|
o.value = c.id;
|
|
|
|
|
|
o.textContent = c.name;
|
|
|
|
|
|
mc.appendChild(o);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (keep) mc.value = keep;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Besorgt einen Sitzungs-Token: vorhandenen vom Broker nutzen, sonst mit dem
|
|
|
|
|
|
// (transienten) Passwortfeld anmelden. Liefert false, wenn kein Passwort da ist.
|
|
|
|
|
|
async function ensureToken() {
|
2026-08-20 14:16:35 +00:00
|
|
|
|
const pw = $("password").value;
|
|
|
|
|
|
// Wurde ein Passwort eingegeben, IMMER frisch anmelden: ein evtl. noch im Broker
|
|
|
|
|
|
// liegender Token könnte zu früheren Zugangsdaten/Server gehören und würde sonst
|
|
|
|
|
|
// zu „GET /FileCabinets -> HTTP 401" führen (bis zum Thunderbird-Neustart).
|
|
|
|
|
|
// Der Broker (background.js) ruft bei auth:logon zuerst Auth.reset() und liest die
|
|
|
|
|
|
// gerade gespeicherten Einstellungen neu.
|
|
|
|
|
|
if (pw) {
|
|
|
|
|
|
const res = await browser.runtime.sendMessage({ type: "auth:logon", password: pw });
|
|
|
|
|
|
if (!res || !res.ok) throw new Error(res && res.error ? res.error : "Anmeldung fehlgeschlagen.");
|
|
|
|
|
|
Auth.setToken(res.token);
|
|
|
|
|
|
$("password").value = ""; // Passwort nicht behalten
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Kein Passwort getippt: vorhandenen Sitzungs-Token vom Broker übernehmen.
|
2026-06-16 12:41:52 +00:00
|
|
|
|
const st = await browser.runtime.sendMessage({ type: "auth:status" });
|
|
|
|
|
|
if (st && st.hasToken && st.token) { Auth.setToken(st.token); return true; }
|
2026-08-20 14:16:35 +00:00
|
|
|
|
return false;
|
2026-06-16 12:41:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 11:23:39 +00:00
|
|
|
|
// --- Feld-Mapping-Editor ---------------------------------------------------
|
|
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
|
|
|
|
|
|
const CUSTOM_OPT = "__custom__"; // Sentinel im Feldname-Dropdown für „eigener Name"
|
|
|
|
|
|
let availableFields = []; // Feldnamen des zuletzt gewählten Archivs
|
|
|
|
|
|
|
|
|
|
|
|
// Gemerkte Feldnamen je Archiv (storage.local, Schlüssel "fieldsByCabinet").
|
|
|
|
|
|
// Ohne diesen Cache war die Auswahlliste bei jedem Thunderbird-Start wieder leer:
|
|
|
|
|
|
// sie kam nur vom Server und setzte damit Passworteingabe + Archivwahl voraus.
|
|
|
|
|
|
let fieldsByCabinet = {};
|
|
|
|
|
|
|
|
|
|
|
|
async function rememberFields(cabinetId, names) {
|
|
|
|
|
|
fieldsByCabinet = { ...fieldsByCabinet, [cabinetId]: names };
|
|
|
|
|
|
await browser.storage.local.set({ fieldsByCabinet }).catch(() => {});
|
|
|
|
|
|
}
|
2026-06-16 13:01:37 +00:00
|
|
|
|
|
|
|
|
|
|
// Zerlegt einen Mapping-Wert (Token-String ODER { const } / { token }) in den UI-Zustand.
|
|
|
|
|
|
function mapValueState(value) {
|
|
|
|
|
|
if (value && typeof value === "object") {
|
|
|
|
|
|
if (value.const != null) return { sel: CONST_OPT, konst: String(value.const) };
|
|
|
|
|
|
if (value.token) return { sel: value.token, konst: "" };
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof value === "string" && value) return { sel: value, konst: "" };
|
|
|
|
|
|
return { sel: "", konst: "" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
// Feldname einer Zeile – aus dem Dropdown oder, bei „Anderer Feldname", aus der
|
|
|
|
|
|
// daneben eingeblendeten Eingabe.
|
|
|
|
|
|
function rowFieldName(row) {
|
|
|
|
|
|
const sel = row.querySelector(".map-field-sel");
|
|
|
|
|
|
if (!sel) return "";
|
|
|
|
|
|
if (sel.value === CUSTOM_OPT) {
|
|
|
|
|
|
const inp = row.querySelector(".map-field-custom");
|
|
|
|
|
|
return inp ? inp.value.trim() : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
return sel.value.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auswahlliste für die Feldname-Dropdowns. Drei Quellen, damit sie NIE leer ist:
|
|
|
|
|
|
// Felder des gewählten Archivs (auch aus dem Cache, also ohne Anmeldung), die
|
|
|
|
|
|
// Standard-Feldnamen und alles im Formular bereits Vergebene.
|
2026-08-21 08:07:41 +00:00
|
|
|
|
function fieldSuggestions() {
|
2026-08-21 08:23:36 +00:00
|
|
|
|
const used = [...document.querySelectorAll("#mapBody .map-row")].map(rowFieldName);
|
2026-08-21 08:07:41 +00:00
|
|
|
|
const all = [...(availableFields || []), ...Object.keys(DEFAULT_FIELD_MAP), ...used];
|
|
|
|
|
|
return [...new Set(all.filter(Boolean))].sort();
|
2026-06-16 14:09:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
// Ein Feldname-Dropdown neu aufbauen. Der aktuelle Wert steht immer mit drin,
|
|
|
|
|
|
// auch wenn das Archiv ihn nicht (mehr) kennt – sonst ginge er beim Neuaufbau
|
|
|
|
|
|
// still verloren.
|
|
|
|
|
|
function fillFieldSelect(sel, current) {
|
|
|
|
|
|
sel.innerHTML = "";
|
|
|
|
|
|
const empty = document.createElement("option");
|
|
|
|
|
|
empty.value = ""; empty.textContent = "— Feld wählen —";
|
|
|
|
|
|
sel.appendChild(empty);
|
|
|
|
|
|
[...new Set([...fieldSuggestions(), current].filter(Boolean))].sort().forEach((n) => {
|
2026-08-21 08:07:41 +00:00
|
|
|
|
const o = document.createElement("option");
|
2026-08-21 08:23:36 +00:00
|
|
|
|
o.value = n; o.textContent = n;
|
|
|
|
|
|
sel.appendChild(o);
|
|
|
|
|
|
});
|
|
|
|
|
|
const own = document.createElement("option");
|
|
|
|
|
|
own.value = CUSTOM_OPT; own.textContent = "✎ Anderer Feldname …";
|
|
|
|
|
|
sel.appendChild(own);
|
|
|
|
|
|
sel.value = current || "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Alle Dropdowns auf die aktuelle Liste bringen (Auswahl bleibt). Zeilen mit
|
|
|
|
|
|
// freier Eingabe bleiben unangetastet.
|
|
|
|
|
|
function refreshFieldSelects() {
|
|
|
|
|
|
document.querySelectorAll("#mapBody .map-field-sel").forEach((sel) => {
|
|
|
|
|
|
if (sel.value === CUSTOM_OPT) return;
|
|
|
|
|
|
fillFieldSelect(sel, sel.value);
|
2026-08-21 08:07:41 +00:00
|
|
|
|
});
|
2026-06-16 14:09:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 13:01:37 +00:00
|
|
|
|
function addMapRow(field = "", value = "") {
|
|
|
|
|
|
const st = mapValueState(value);
|
2026-06-16 14:09:04 +00:00
|
|
|
|
const row = document.createElement("div");
|
|
|
|
|
|
row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : "");
|
|
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
// Links gestapelt: Feldname (Dropdown, bei Bedarf freie Eingabe) über Mail-Attribut.
|
2026-06-16 14:09:04 +00:00
|
|
|
|
const left = document.createElement("div"); left.className = "map-left";
|
2026-06-16 11:23:39 +00:00
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
// Feldname wird ausgewählt. Die Liste ist immer gefüllt (Archiv-Cache +
|
|
|
|
|
|
// Standardnamen), und „✎ Anderer Feldname" blendet eine freie Eingabe ein –
|
|
|
|
|
|
// für Felder, die in keiner Liste stehen.
|
|
|
|
|
|
const fieldSel = document.createElement("select");
|
|
|
|
|
|
fieldSel.className = "map-field-sel";
|
|
|
|
|
|
|
|
|
|
|
|
const own = document.createElement("input");
|
|
|
|
|
|
own.type = "text"; own.className = "map-field-custom";
|
|
|
|
|
|
own.placeholder = "Eigener Feldname, z. B. EML_SUBJECT";
|
|
|
|
|
|
own.hidden = true;
|
|
|
|
|
|
|
|
|
|
|
|
fillFieldSelect(fieldSel, field);
|
|
|
|
|
|
if (field && fieldSel.value !== field) {
|
|
|
|
|
|
// Sicherheitsnetz: Name nicht im Dropdown gelandet -> als freie Eingabe zeigen,
|
|
|
|
|
|
// statt ihn stillschweigend zu verlieren.
|
|
|
|
|
|
fieldSel.value = CUSTOM_OPT; own.value = field; own.hidden = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fieldSel.addEventListener("change", () => {
|
|
|
|
|
|
const isOwn = fieldSel.value === CUSTOM_OPT;
|
|
|
|
|
|
own.hidden = !isOwn;
|
|
|
|
|
|
if (isOwn) own.focus();
|
|
|
|
|
|
});
|
|
|
|
|
|
own.addEventListener("change", refreshFieldSelects);
|
2026-06-16 11:23:39 +00:00
|
|
|
|
|
2026-06-16 14:09:04 +00:00
|
|
|
|
const src = document.createElement("select"); src.className = "map-source";
|
2026-06-16 11:23:39 +00:00
|
|
|
|
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
|
2026-06-16 14:09:04 +00:00
|
|
|
|
src.appendChild(none);
|
2026-06-16 11:23:39 +00:00
|
|
|
|
MAIL_TOKENS.forEach((t) => {
|
|
|
|
|
|
const o = document.createElement("option");
|
|
|
|
|
|
o.value = t.key; o.textContent = t.label;
|
2026-06-16 13:01:37 +00:00
|
|
|
|
if (t.key === st.sel) o.selected = true;
|
2026-06-16 14:09:04 +00:00
|
|
|
|
src.appendChild(o);
|
2026-06-16 11:23:39 +00:00
|
|
|
|
});
|
2026-06-16 13:01:37 +00:00
|
|
|
|
const co = document.createElement("option");
|
|
|
|
|
|
co.value = CONST_OPT; co.textContent = "★ Fester Wert";
|
|
|
|
|
|
if (st.sel === CONST_OPT) co.selected = true;
|
2026-06-16 14:09:04 +00:00
|
|
|
|
src.appendChild(co);
|
2026-06-16 13:01:37 +00:00
|
|
|
|
|
2026-08-21 08:23:36 +00:00
|
|
|
|
left.append(fieldSel, own, src);
|
2026-06-16 13:01:37 +00:00
|
|
|
|
|
2026-06-16 14:09:04 +00:00
|
|
|
|
// Rechts (breit): fester Wert – nur sichtbar bei „Fester Wert".
|
|
|
|
|
|
const right = document.createElement("div"); right.className = "map-right";
|
|
|
|
|
|
const konst = document.createElement("input");
|
|
|
|
|
|
konst.type = "text"; konst.className = "map-const"; konst.placeholder = "Fester Wert, z. B. NEU";
|
|
|
|
|
|
konst.value = st.konst;
|
|
|
|
|
|
right.appendChild(konst);
|
|
|
|
|
|
|
|
|
|
|
|
src.addEventListener("change", () => {
|
|
|
|
|
|
const isConst = src.value === CONST_OPT;
|
|
|
|
|
|
row.classList.toggle("has-const", isConst);
|
|
|
|
|
|
if (isConst) konst.focus();
|
2026-06-16 13:01:37 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-16 11:23:39 +00:00
|
|
|
|
const del = document.createElement("button");
|
2026-06-16 12:41:52 +00:00
|
|
|
|
del.type = "button"; del.className = "icon-btn"; del.textContent = "✕"; del.title = "Zeile entfernen";
|
2026-06-16 14:09:04 +00:00
|
|
|
|
del.addEventListener("click", () => row.remove());
|
2026-06-16 11:23:39 +00:00
|
|
|
|
|
2026-06-16 14:09:04 +00:00
|
|
|
|
row.append(left, right, del);
|
|
|
|
|
|
$("mapBody").appendChild(row);
|
2026-06-16 11:23:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function loadMapping(map) {
|
|
|
|
|
|
$("mapBody").innerHTML = "";
|
2026-06-16 13:01:37 +00:00
|
|
|
|
Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v));
|
2026-08-21 08:07:41 +00:00
|
|
|
|
refreshFieldSelects();
|
2026-06-16 11:23:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function collectMapping() {
|
|
|
|
|
|
const map = {};
|
2026-06-16 14:09:04 +00:00
|
|
|
|
document.querySelectorAll("#mapBody .map-row").forEach((row) => {
|
2026-08-21 08:23:36 +00:00
|
|
|
|
const f = rowFieldName(row);
|
2026-06-16 13:01:37 +00:00
|
|
|
|
if (!f) return; // ohne Feldname ignorieren
|
2026-06-16 14:09:04 +00:00
|
|
|
|
const sel = row.querySelector(".map-source").value;
|
2026-06-16 13:01:37 +00:00
|
|
|
|
if (sel === CONST_OPT) {
|
2026-06-16 14:09:04 +00:00
|
|
|
|
const c = row.querySelector(".map-const").value;
|
2026-06-16 13:01:37 +00:00
|
|
|
|
if (c !== "") map[f] = { const: c }; // fester Wert
|
|
|
|
|
|
} else if (sel) {
|
|
|
|
|
|
map[f] = sel; // Mail-Token (als String, abwärtskompatibel)
|
|
|
|
|
|
}
|
2026-06-16 11:23:39 +00:00
|
|
|
|
});
|
|
|
|
|
|
return map;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
function collect() {
|
|
|
|
|
|
const partial = {};
|
|
|
|
|
|
FIELDS.forEach((f) => (partial[f] = $(f).value.trim()));
|
|
|
|
|
|
CHECKS.forEach((c) => (partial[c] = $(c).checked));
|
|
|
|
|
|
partial.defaultCabinetId = $("defaultCabinet").value;
|
2026-06-16 11:23:39 +00:00
|
|
|
|
partial.mailFormat = getMailFormat();
|
|
|
|
|
|
partial.fieldMapping = collectMapping();
|
2026-06-16 13:01:37 +00:00
|
|
|
|
partial.mappingUrl = $("mappingUrl").value.trim();
|
2026-06-03 11:15:59 +00:00
|
|
|
|
return partial;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 12:41:52 +00:00
|
|
|
|
// --- Events ----------------------------------------------------------------
|
2026-06-16 11:23:39 +00:00
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
$("save").addEventListener("click", async () => {
|
2026-06-17 11:43:59 +00:00
|
|
|
|
const data = collect();
|
2026-08-20 14:16:35 +00:00
|
|
|
|
const saved = await Settings.set(data);
|
|
|
|
|
|
// Server/Org/Benutzer geändert? Dann gehört ein evtl. noch im Broker liegender
|
|
|
|
|
|
// Token zu den ALTEN Daten -> verwerfen, damit Ablage/Test sich neu anmelden.
|
|
|
|
|
|
const wasServerChange = serverChanged(saved); // vor dem Neu-Schnappschuss prüfen
|
|
|
|
|
|
await invalidateSession(saved);
|
2026-06-17 11:43:59 +00:00
|
|
|
|
const warn = insecureWarning(data.serverUrl);
|
2026-08-20 14:16:35 +00:00
|
|
|
|
const note = wasServerChange ? " Neuer Server – bitte Verbindung testen und Standardziel neu wählen." : "";
|
|
|
|
|
|
setStatus("Gespeichert." + note + warn, warn ? "err" : "ok");
|
2026-06-03 11:15:59 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$("test").addEventListener("click", async () => {
|
|
|
|
|
|
setStatus("Verbinde …");
|
|
|
|
|
|
try {
|
2026-08-20 14:16:35 +00:00
|
|
|
|
// Erst speichern (ohne Passwort), dann testen. Bei geänderten Zugangsdaten
|
|
|
|
|
|
// vorher den alten Broker-Token verwerfen (sonst HTTP 401).
|
|
|
|
|
|
const s = await invalidateSession(await Settings.set(collect()));
|
2026-06-16 12:41:52 +00:00
|
|
|
|
const ok = await ensureToken();
|
|
|
|
|
|
if (!ok) { setStatus("Bitte Passwort eingeben.", "err"); return; }
|
2026-06-03 11:15:59 +00:00
|
|
|
|
const cabinets = await DocuWare.listCabinets(s);
|
|
|
|
|
|
await browser.storage.local.set({ cabinets });
|
|
|
|
|
|
fillCabinets(cabinets, s.defaultCabinetId);
|
|
|
|
|
|
const arch = cabinets.filter((c) => !c.isBasket).length;
|
|
|
|
|
|
const bk = cabinets.filter((c) => c.isBasket).length;
|
2026-06-17 11:43:59 +00:00
|
|
|
|
const warn = insecureWarning(s.serverUrl);
|
|
|
|
|
|
setStatus(`Verbindung ok – ${arch} Archive, ${bk} Briefkörbe gefunden.` + warn, warn ? "err" : "ok");
|
2026-06-03 11:15:59 +00:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setStatus(`Fehler: ${e.message}`, "err");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-16 12:41:52 +00:00
|
|
|
|
$("mapAdd").addEventListener("click", () => addMapRow());
|
|
|
|
|
|
$("mapReset").addEventListener("click", () => loadMapping(DEFAULT_FIELD_MAP));
|
2026-06-03 11:15:59 +00:00
|
|
|
|
|
2026-06-16 13:01:37 +00:00
|
|
|
|
// --- Mapping teilen: JSON anzeigen / kopieren / anwenden / von URL laden ----
|
|
|
|
|
|
|
|
|
|
|
|
$("mappingExport").addEventListener("click", () => {
|
|
|
|
|
|
$("mappingJson").value = JSON.stringify(collectMapping(), null, 2);
|
|
|
|
|
|
setStatus("Aktuelles Mapping als JSON angezeigt.", "ok");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$("mappingCopy").addEventListener("click", async () => {
|
|
|
|
|
|
let txt = $("mappingJson").value.trim();
|
|
|
|
|
|
if (!txt) { txt = JSON.stringify(collectMapping(), null, 2); $("mappingJson").value = txt; }
|
|
|
|
|
|
try {
|
|
|
|
|
|
await navigator.clipboard.writeText(txt);
|
|
|
|
|
|
setStatus("In Zwischenablage kopiert.", "ok");
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
$("mappingJson").select(); document.execCommand("copy");
|
|
|
|
|
|
setStatus("In Zwischenablage kopiert.", "ok");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$("mappingApply").addEventListener("click", () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const obj = JSON.parse($("mappingJson").value);
|
|
|
|
|
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("kein JSON-Objekt");
|
|
|
|
|
|
loadMapping(obj);
|
|
|
|
|
|
setStatus("JSON ins Mapping übernommen – mit „Speichern“ sichern.", "ok");
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setStatus(`Ungültiges JSON: ${e.message}`, "err");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$("mappingPull").addEventListener("click", async () => {
|
|
|
|
|
|
const url = $("mappingUrl").value.trim();
|
|
|
|
|
|
if (!url) { setStatus("Bitte eine URL eingeben.", "err"); return; }
|
|
|
|
|
|
setStatus("Lade Mapping …");
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(url, { cache: "no-store" });
|
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
|
|
|
|
const obj = await res.json();
|
|
|
|
|
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("kein JSON-Objekt");
|
|
|
|
|
|
loadMapping(obj);
|
|
|
|
|
|
await Settings.set({ mappingUrl: url }); // URL gleich merken
|
|
|
|
|
|
setStatus("Mapping von URL geladen – mit „Speichern“ übernehmen.", "ok");
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setStatus(`Laden fehlgeschlagen: ${e.message}`, "err");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-21 08:07:41 +00:00
|
|
|
|
// Archiv wählen -> dessen Indexfelder als Vorschläge für alle Feldname-Eingaben.
|
2026-06-16 12:41:52 +00:00
|
|
|
|
$("mapCabinet").addEventListener("change", async () => {
|
|
|
|
|
|
const id = $("mapCabinet").value;
|
|
|
|
|
|
const st = $("mapFieldStatus");
|
2026-06-16 14:09:04 +00:00
|
|
|
|
if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; }
|
2026-08-21 08:23:36 +00:00
|
|
|
|
|
|
|
|
|
|
// Gemerkte Liste sofort anzeigen – dann ist die Auswahl auch ohne Anmeldung da.
|
|
|
|
|
|
if (Array.isArray(fieldsByCabinet[id])) {
|
|
|
|
|
|
availableFields = fieldsByCabinet[id];
|
|
|
|
|
|
refreshFieldSelects();
|
|
|
|
|
|
st.textContent = `${availableFields.length} Felder zur Auswahl (gemerkt).`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
st.textContent = "Lade Felder …";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
try {
|
2026-06-16 12:41:52 +00:00
|
|
|
|
const s = await Settings.get();
|
|
|
|
|
|
const ok = await ensureToken();
|
2026-08-21 08:23:36 +00:00
|
|
|
|
if (!ok) {
|
|
|
|
|
|
if (!availableFields.length) st.textContent = "Erst „Verbindung testen“ (Passwort nötig).";
|
|
|
|
|
|
return; // gemerkte Liste bleibt stehen
|
|
|
|
|
|
}
|
2026-06-16 14:09:04 +00:00
|
|
|
|
availableFields = (await DocuWare.cabinetFieldNames(s, id))
|
|
|
|
|
|
.filter((n) => n && !/^DW[A-Z]/.test(n)) // DW*-Systemfelder ausblenden
|
|
|
|
|
|
.sort();
|
2026-08-21 08:23:36 +00:00
|
|
|
|
await rememberFields(id, availableFields);
|
2026-06-16 14:09:04 +00:00
|
|
|
|
refreshFieldSelects();
|
|
|
|
|
|
st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
|
2026-06-03 11:15:59 +00:00
|
|
|
|
} catch (e) {
|
2026-06-16 12:41:52 +00:00
|
|
|
|
st.textContent = `Fehler: ${e.message}`;
|
2026-06-03 11:15:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
load();
|