Optionen: DocuWare-Dark-Style, in Karten aufgeteilt; Export/Import + Diagnose entfernt; Mapping mit Archiv-Feldvorschlägen (datalist)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sylyx 2026-06-16 14:41:52 +02:00
parent c7f205d9bc
commit 52e8942e6d
2 changed files with 272 additions and 175 deletions

View File

@ -4,33 +4,154 @@
<meta charset="utf-8" />
<title>DocuWare Ablage Einstellungen</title>
<style>
body { font: 14px/1.5 system-ui, sans-serif; max-width: 560px; margin: 24px auto; padding: 0 16px; color: #1a1a1a; }
h1 { font-size: 20px; }
fieldset { border: 1px solid #ddd; border-radius: 8px; margin: 16px 0; padding: 12px 16px; }
legend { font-weight: 600; padding: 0 6px; }
label { display: block; margin: 10px 0 4px; font-weight: 500; }
input[type="text"], input[type="password"], input[type="url"] { width: 100%; padding: 7px 9px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; }
.check { display: flex; align-items: center; gap: 8px; font-weight: 400; margin: 8px 0; }
.row { display: flex; gap: 12px; align-items: center; margin-top: 16px; }
button { padding: 8px 16px; border: 0; border-radius: 6px; background: #2563eb; color: #fff; font-weight: 600; cursor: pointer; }
button.secondary { background: #e5e7eb; color: #111; }
/* DocuWare-naher Dark-Style: tiefes Slate, DocuWare-Blau als Akzent. */
:root {
--bg: #11151b;
--bg-grad: radial-gradient(1200px 500px at 80% -10%, #18324a 0%, rgba(17,21,27,0) 60%);
--surface: #1a1f27;
--surface-2: #222a34;
--border: #2b3340;
--border-strong: #38424f;
--text: #e8edf2;
--muted: #8c97a4;
--accent: #2aa4df; /* DocuWare-Blau */
--accent-hover: #3cb6f0;
--accent-weak: rgba(42,164,223,.14);
--ok: #46c98a;
--err: #ec6b75;
--radius: 10px;
--shadow: 0 1px 0 rgba(255,255,255,.02) inset, 0 8px 24px rgba(0,0,0,.28);
font-synthesis: none;
}
* { box-sizing: border-box; }
html, body { background: var(--bg); }
body {
font: 14px/1.55 "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
color: var(--text);
margin: 0;
padding: 0 16px 120px;
background-image: var(--bg-grad);
background-repeat: no-repeat;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 620px; margin: 0 auto; }
/* Kopfzeile */
header.app {
display: flex; align-items: center; gap: 12px;
padding: 26px 2px 18px;
}
.logo {
width: 34px; height: 34px; border-radius: 8px; flex: none;
background: linear-gradient(135deg, var(--accent), #1c6fae);
display: grid; place-items: center;
box-shadow: 0 4px 14px rgba(42,164,223,.35);
}
.logo svg { width: 20px; height: 20px; }
header.app h1 { font-size: 18px; font-weight: 650; margin: 0; letter-spacing: .2px; }
header.app .sub { font-size: 12px; color: var(--muted); margin-top: 1px; }
/* Karten / Abschnitte */
section.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px 20px;
margin: 14px 0;
}
.card-head { display: flex; align-items: baseline; gap: 10px; margin: 0 0 6px; }
.card-head h2 {
font-size: 13px; font-weight: 650; text-transform: uppercase;
letter-spacing: .8px; color: var(--accent); margin: 0;
}
.card-head .num {
font-size: 11px; color: var(--muted); border: 1px solid var(--border-strong);
border-radius: 999px; padding: 1px 8px; line-height: 1.5;
}
label { display: block; margin: 14px 0 5px; font-weight: 550; font-size: 13px; }
input[type="text"], input[type="password"], input[type="url"], select {
width: 100%; padding: 9px 11px; color: var(--text);
background: var(--surface-2); border: 1px solid var(--border-strong);
border-radius: 8px; font: inherit; outline: none;
transition: border-color .15s, box-shadow .15s, background .15s;
}
select { appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4l4 4 4-4' fill='none' stroke='%238c97a4' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>");
background-repeat: no-repeat; background-position: right 11px center; padding-right: 30px;
}
input:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-weak);
}
input::placeholder { color: #5f6b78; }
code { background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px;
padding: 1px 5px; font-size: 12px; color: #b7c2cd; }
.hint { font-size: 12px; color: var(--muted); margin-top: 5px; }
.check { display: flex; align-items: center; gap: 9px; margin: 9px 0; font-weight: 450; }
.check label { margin: 0; font-weight: 450; font-size: 13px; }
input[type="checkbox"], input[type="radio"] { width: 16px; height: 16px; accent-color: var(--accent); flex: none; }
/* Mapping-Tabelle */
.map-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 4px 0 12px; }
.map-toolbar .grow { flex: 1; min-width: 180px; }
table.map { width: 100%; border-collapse: separate; border-spacing: 0 6px; }
table.map th { text-align: left; font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
color: var(--muted); font-weight: 600; padding: 0 6px 2px; }
table.map td { padding: 0 6px; vertical-align: middle; }
table.map input, table.map select { padding: 7px 9px; }
table.map td.del { width: 40px; }
.icon-btn {
width: 30px; height: 30px; padding: 0; border-radius: 7px; cursor: pointer;
background: var(--surface-2); border: 1px solid var(--border-strong); color: var(--muted);
font-size: 14px; line-height: 1; transition: .15s;
}
.icon-btn:hover { color: var(--err); border-color: var(--err); }
/* Buttons */
button { font: inherit; cursor: pointer; border-radius: 8px; }
button.primary {
padding: 9px 18px; border: 0; font-weight: 650; color: #04222f;
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
box-shadow: 0 4px 14px rgba(42,164,223,.3);
}
button.primary:hover { filter: brightness(1.06); }
button.ghost {
padding: 8px 15px; font-weight: 550; color: var(--text);
background: var(--surface-2); border: 1px solid var(--border-strong);
}
button.ghost:hover { border-color: var(--accent); color: var(--accent); }
button:disabled { opacity: .5; cursor: default; }
/* Sticky Aktionsleiste */
.actionbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 5;
background: rgba(17,21,27,.82); backdrop-filter: blur(8px);
border-top: 1px solid var(--border);
}
.actionbar .inner { max-width: 620px; margin: 0 auto; padding: 12px 16px;
display: flex; align-items: center; gap: 12px; }
#status { font-size: 13px; min-height: 18px; }
.ok { color: #15803d; } .err { color: #b91c1c; }
.hint { font-size: 12px; color: #666; margin-top: 2px; }
table.map { width: 100%; border-collapse: collapse; margin-top: 8px; }
table.map th { text-align: left; font-size: 12px; color: #666; font-weight: 600; padding: 2px 4px; }
table.map td { padding: 3px 4px; vertical-align: middle; }
table.map input, table.map select { width: 100%; padding: 5px 7px; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; }
table.map td.del { width: 36px; }
table.map button { padding: 5px 9px; }
button.secondary { background: #e5e7eb; color: #111; }
.ok { color: var(--ok); } .err { color: var(--err); }
.spacer { flex: 1; }
</style>
</head>
<body>
<h1>DocuWare Ablage Einstellungen</h1>
<div class="wrap">
<header class="app">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4H13l3 3h2.5A1.5 1.5 0 0 1 20 8.5v10A1.5 1.5 0 0 1 18.5 20h-13A1.5 1.5 0 0 1 4 18.5v-13Z" fill="#fff" opacity=".95"/><path d="M9 13.5h6M12 11v5" stroke="#1c6fae" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
<div>
<h1>DocuWare Ablage</h1>
<div class="sub">Einstellungen</div>
</div>
</header>
<fieldset>
<legend>Verbindung</legend>
<section class="card">
<div class="card-head"><span class="num">1</span><h2>Verbindung</h2></div>
<label for="serverUrl">Server-URL</label>
<input type="url" id="serverUrl" placeholder="https://docuware.example.com" />
<div class="hint">Basis-URL ohne <code>/DocuWare/Platform</code>.</div>
@ -45,68 +166,57 @@
<input type="password" id="password" autocomplete="off" />
<div class="hint">Wird <strong>nicht gespeichert</strong> nur zum Anmelden/Testen für
diese Sitzung. Im Ablage-Fenster wird es 1× pro Sitzung abgefragt.</div>
</fieldset>
</section>
<fieldset>
<legend>Standardwerte</legend>
<section class="card">
<div class="card-head"><span class="num">2</span><h2>Standardwerte</h2></div>
<label for="defaultCabinet">Standard-Ziel (Archiv oder Briefkorb)</label>
<select id="defaultCabinet" style="width:100%;padding:7px;border:1px solid #ccc;border-radius:6px;">
<option value="">— keiner —</option>
</select>
<select id="defaultCabinet"><option value="">— keiner —</option></select>
<div class="hint">Liste wird nach erfolgreichem Verbindungstest gefüllt.</div>
<label>Standard-Format der E-Mail</label>
<div class="check"><input type="radio" name="mailFormat" id="fmtEml" value="eml" /> <label for="fmtEml" style="margin:0">Original (.eml)</label></div>
<div class="check"><input type="radio" name="mailFormat" id="fmtPdf" value="pdf" /> <label for="fmtPdf" style="margin:0">PDF</label></div>
<div class="check"><input type="radio" name="mailFormat" id="fmtEml" value="eml" /> <label for="fmtEml">Original (.eml)</label></div>
<div class="check"><input type="radio" name="mailFormat" id="fmtPdf" value="pdf" /> <label for="fmtPdf">PDF</label></div>
<div class="hint">Die E-Mail wird immer in genau einem Format abgelegt (nicht beides). Im Ablage-Fenster umstellbar.</div>
<div class="check" style="margin-top:10px"><input type="checkbox" id="storeAttachments" /> <label for="storeAttachments" style="margin:0">Anhänge mit ablegen</label></div>
<div class="check"><input type="checkbox" id="tagOnSuccess" /> <label for="tagOnSuccess" style="margin:0">Mail nach Ablage als „DocuWare" markieren</label></div>
</fieldset>
<div class="check" style="margin-top:12px"><input type="checkbox" id="storeAttachments" /> <label for="storeAttachments">Anhänge mit ablegen</label></div>
<div class="check"><input type="checkbox" id="tagOnSuccess" /> <label for="tagOnSuccess">Mail nach Ablage als „DocuWare" markieren</label></div>
</section>
<div class="row">
<button id="save">Speichern</button>
<button id="test" class="secondary">Verbindung testen</button>
<span id="status"></span>
<section class="card">
<div class="card-head"><span class="num">3</span><h2>Feld-Zuordnung</h2></div>
<div class="hint" style="margin-bottom:10px">Welches DocuWare-Indexfeld wird aus welchem
Mail-Attribut vorbefüllt? Gilt für alle Aktenschränke. Leere/entfernte Zeilen befüllen
nichts vor.</div>
<div class="map-toolbar">
<div class="grow">
<select id="mapCabinet"><option value="">Feldnamen aus Archiv vorschlagen …</option></select>
</div>
<span id="mapFieldStatus" class="hint" style="margin:0"></span>
</div>
<fieldset>
<legend>Feld-Zuordnung (Vorbefüllung)</legend>
<div class="hint">Legt fest, welches DocuWare-Indexfeld aus welchem Mail-Attribut vorbefüllt
wird. <strong>Feldname</strong> = DocuWare-Datenbankname (z. B. <code>EML_SUBJECT</code>).
Gilt für alle Aktenschränke. Leere/entfernte Zeilen befüllen nichts vor.</div>
<table class="map">
<thead><tr><th>DocuWare-Feldname</th><th>Mail-Attribut</th><th></th></tr></thead>
<tbody id="mapBody"></tbody>
</table>
<div class="row">
<button id="mapAdd" class="secondary" type="button">+ Zeile</button>
<button id="mapReset" class="secondary" type="button">Auf Standard zurücksetzen</button>
</div>
</fieldset>
<datalist id="fieldNames"></datalist>
<fieldset>
<legend>Einstellungen sichern</legend>
<div class="hint">Exportiert/importiert die Einstellungen (Server, Organisation,
Benutzer, Standardwerte) als Datei. <strong>Das Passwort ist nicht enthalten</strong>
und muss nach einer Neuinstallation einmal neu eingegeben werden.</div>
<div class="row">
<button id="exportBtn" class="secondary">Exportieren</button>
<button id="importBtn" class="secondary">Importieren</button>
<input type="file" id="importFile" accept="application/json,.json" style="display:none" />
<div class="map-toolbar" style="margin-top:10px">
<button id="mapAdd" class="ghost" type="button">+ Zeile</button>
<button id="mapReset" class="ghost" type="button">Auf Standard zurücksetzen</button>
</div>
</section>
</div>
</fieldset>
<fieldset>
<legend>Diagnose (für Entwicklung)</legend>
<div class="hint">Analysiert den Store-Dialog des oben gewählten Standard-Ziels und
zeigt die Rohstruktur (Felder, Auswahllisten, Defaults). Inhalt bitte kopieren und
an den Entwickler geben.</div>
<div class="row">
<button id="diagBtn" class="secondary">Store-Dialog analysieren</button>
<div class="actionbar">
<div class="inner">
<button id="save" class="primary">Speichern</button>
<button id="test" class="ghost">Verbindung testen</button>
<span class="spacer"></span>
<span id="status"></span>
</div>
</div>
<textarea id="diag" readonly style="width:100%;height:240px;margin-top:10px;font:12px/1.4 monospace;border:1px solid #ccc;border-radius:6px;padding:8px;display:none;"></textarea>
</fieldset>
<script src="../lib/store.js"></script>
<script src="../lib/auth.js"></script>

View File

@ -32,6 +32,53 @@ function getMailFormat() {
return el ? el.value : "eml";
}
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() {
const st = await browser.runtime.sendMessage({ type: "auth:status" });
if (st && st.hasToken && st.token) { Auth.setToken(st.token); return true; }
const pw = $("password").value;
if (!pw) return false;
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;
}
// --- Feld-Mapping-Editor ---------------------------------------------------
function addMapRow(field = "", token = "") {
@ -40,6 +87,7 @@ function addMapRow(field = "", token = "") {
const inp = document.createElement("input");
inp.type = "text"; inp.className = "map-field"; inp.value = field;
inp.placeholder = "EML_SUBJECT";
inp.setAttribute("list", "fieldNames"); // Vorschläge aus gewähltem Archiv
const tdF = document.createElement("td"); tdF.appendChild(inp);
const sel = document.createElement("select"); sel.className = "map-token";
@ -54,7 +102,7 @@ function addMapRow(field = "", token = "") {
const tdT = document.createElement("td"); tdT.appendChild(sel);
const del = document.createElement("button");
del.type = "button"; del.className = "secondary"; del.textContent = "✕";
del.type = "button"; del.className = "icon-btn"; del.textContent = "✕"; del.title = "Zeile entfernen";
del.addEventListener("click", () => tr.remove());
const tdX = document.createElement("td"); tdX.className = "del"; tdX.appendChild(del);
@ -77,26 +125,6 @@ function collectMapping() {
return map;
}
function fillCabinets(cabinets, selectedId) {
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));
}
function collect() {
const partial = {};
FIELDS.forEach((f) => (partial[f] = $(f).value.trim()));
@ -107,8 +135,7 @@ function collect() {
return partial;
}
$("mapAdd").addEventListener("click", () => addMapRow());
$("mapReset").addEventListener("click", () => loadMapping(DEFAULT_FIELD_MAP));
// --- Events ----------------------------------------------------------------
$("save").addEventListener("click", async () => {
await Settings.set(collect());
@ -116,17 +143,11 @@ $("save").addEventListener("click", async () => {
});
$("test").addEventListener("click", async () => {
const pw = $("password").value;
if (!pw) { setStatus("Bitte Passwort eingeben.", "err"); return; }
setStatus("Verbinde …");
try {
const s = await Settings.set(collect()); // erst speichern (ohne Passwort), dann testen
// Anmeldung über den Hintergrund-Broker: primt zugleich die Sitzung, sodass
// das Ablege-Fenster danach nicht erneut nach dem Passwort fragt.
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
const ok = await ensureToken();
if (!ok) { setStatus("Bitte Passwort eingeben.", "err"); return; }
const cabinets = await DocuWare.listCabinets(s);
await browser.storage.local.set({ cabinets });
fillCabinets(cabinets, s.defaultCabinetId);
@ -138,65 +159,31 @@ $("test").addEventListener("click", async () => {
}
});
$("exportBtn").addEventListener("click", async () => {
$("mapAdd").addEventListener("click", () => addMapRow());
$("mapReset").addEventListener("click", () => loadMapping(DEFAULT_FIELD_MAP));
// Archiv wählen -> dessen Indexfelder als Vorschläge (datalist) für alle Feldnamen.
$("mapCabinet").addEventListener("change", async () => {
const id = $("mapCabinet").value;
const dl = $("fieldNames");
const st = $("mapFieldStatus");
if (!id) { dl.innerHTML = ""; st.textContent = ""; return; }
st.textContent = "Lade Felder …";
try {
const s = await Settings.get();
const blob = new Blob([JSON.stringify(s, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "docuware-ablage-einstellungen.json";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
setStatus("Einstellungen exportiert.", "ok");
const ok = await ensureToken();
if (!ok) { st.textContent = "Erst „Verbindung testen“ (Passwort nötig)."; return; }
const names = (await DocuWare.cabinetFieldNames(s, id))
.filter((n) => n && !/^DW[A-Z]/.test(n)); // DW*-Systemfelder ausblenden
dl.innerHTML = "";
names.sort().forEach((n) => {
const o = document.createElement("option");
o.value = n;
dl.appendChild(o);
});
$("importBtn").addEventListener("click", () => $("importFile").click());
$("importFile").addEventListener("change", async (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
await Settings.set(data);
await load();
setStatus("Einstellungen importiert.", "ok");
} catch (err) {
setStatus(`Import fehlgeschlagen: ${err.message}`, "err");
} finally {
e.target.value = "";
}
});
$("diagBtn").addEventListener("click", async () => {
const cabinetId = $("defaultCabinet").value;
const out = $("diag");
out.style.display = "block";
if (!cabinetId) {
out.value = "Bitte oben zuerst ein Standard-Ziel (Archiv) wählen.";
return;
}
out.value = "Analysiere …";
try {
const s = await Settings.set(collect());
// Sitzungs-Token nutzen; falls noch keiner da ist, Passwortfeld verwenden.
const st = await browser.runtime.sendMessage({ type: "auth:status" });
if (st && st.hasToken && st.token) {
Auth.setToken(st.token);
} else {
const pw = $("password").value;
if (!pw) { out.value = "Bitte Passwort eingeben (oder zuerst „Verbindung testen“)."; return; }
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 = "";
}
const dump = await DocuWare.diagnoseStoreDialog(s, cabinetId);
out.value = JSON.stringify(dump, null, 2);
st.textContent = `${names.length} Felder als Vorschlag geladen.`;
} catch (e) {
out.value = `Fehler: ${e.message}`;
st.textContent = `Fehler: ${e.message}`;
}
});