Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d02228e4c9 | |||
| 589c0018a9 | |||
| 03baacd5d8 | |||
| ea6a518c9c | |||
| 3425e3cafd | |||
| 51f9362c9c | |||
| 1a358a96ef | |||
| 81964c76d3 | |||
| 2194cc2d56 | |||
| 52e8942e6d | |||
| c7f205d9bc | |||
| 53d3675dc1 |
@ -1,94 +1,127 @@
|
|||||||
|
/* DocuWare-naher Dark-Style – identische Palette wie die Optionsseite. */
|
||||||
|
:root {
|
||||||
|
--bg: #11151b;
|
||||||
|
--surface: #1a1f27;
|
||||||
|
--surface-2: #222a34;
|
||||||
|
--border: #2b3340;
|
||||||
|
--border-strong: #38424f;
|
||||||
|
--text: #e8edf2;
|
||||||
|
--muted: #8c97a4;
|
||||||
|
--accent: #2aa4df;
|
||||||
|
--accent-hover: #3cb6f0;
|
||||||
|
--accent-weak: rgba(42,164,223,.14);
|
||||||
|
--ok: #46c98a;
|
||||||
|
--err: #ec6b75;
|
||||||
|
color-scheme: dark; /* native Datepicker/Scrollbars/Checkboxen im Dark-Mode */
|
||||||
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
font: 13px/1.45 "Segoe UI", system-ui, sans-serif;
|
font: 13px/1.45 "Segoe UI", "Helvetica Neue", system-ui, sans-serif;
|
||||||
margin: 0; color: #1a1a1a; background: #fff;
|
margin: 0; color: var(--text); background: var(--bg);
|
||||||
display: flex; flex-direction: column; height: 100vh;
|
display: flex; flex-direction: column; height: 100vh;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Titel-Leiste wie im Connect-to-Outlook-Dialog */
|
/* Titel-Leiste */
|
||||||
.titlebar {
|
.titlebar {
|
||||||
background: #2b3a44; color: #fff;
|
background: #0f1318; color: var(--text);
|
||||||
padding: 8px 14px; font-weight: 600; font-size: 13px;
|
padding: 9px 16px; font-weight: 600; font-size: 13px; letter-spacing: .2px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
box-shadow: 0 1px 0 rgba(42,164,223,.35);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
header { padding: 12px 18px 6px; border-bottom: 1px solid #e5e7eb; flex-shrink: 0; }
|
header { padding: 14px 18px 8px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||||
header h1 { font-size: 16px; font-weight: 600; margin: 0 0 10px; }
|
header h1 { font-size: 16px; font-weight: 650; margin: 0 0 10px; }
|
||||||
|
|
||||||
.toolbar { display: flex; align-items: center; gap: 14px; }
|
.toolbar { display: flex; align-items: center; gap: 14px; }
|
||||||
.toolbar .spacer { flex: 1; }
|
.toolbar .spacer { flex: 1; }
|
||||||
|
button { font: inherit; cursor: pointer; }
|
||||||
button.link {
|
button.link {
|
||||||
background: none; border: 0; color: #1f2937; cursor: pointer;
|
background: none; border: 0; color: var(--muted);
|
||||||
font: inherit; padding: 4px 2px; display: inline-flex; align-items: center; gap: 5px;
|
padding: 4px 2px; display: inline-flex; align-items: center; gap: 5px;
|
||||||
}
|
}
|
||||||
button.link:hover { color: #2563eb; }
|
button.link:hover { color: var(--accent); }
|
||||||
button.primary {
|
button.primary {
|
||||||
background: #f4b400; color: #1a1a1a; border: 0; border-radius: 3px;
|
border: 0; border-radius: 8px; padding: 8px 22px; font-weight: 650; color: #04222f;
|
||||||
padding: 7px 22px; font-weight: 600; cursor: pointer;
|
background: linear-gradient(180deg, var(--accent-hover), var(--accent));
|
||||||
|
box-shadow: 0 4px 14px rgba(42,164,223,.3);
|
||||||
}
|
}
|
||||||
button.primary:hover { background: #e0a500; }
|
button.primary:hover { filter: brightness(1.06); }
|
||||||
button:disabled { opacity: .5; cursor: default; }
|
button:disabled { opacity: .5; cursor: default; filter: none; }
|
||||||
|
|
||||||
main { flex: 1; overflow-y: auto; padding: 14px 18px; }
|
main { flex: 1; overflow-y: auto; padding: 16px 18px; }
|
||||||
|
|
||||||
.target { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; margin-bottom: 12px; }
|
.target { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||||
.target > label { text-align: right; color: #374151; font-weight: 500; }
|
.target > label { text-align: right; color: var(--muted); font-weight: 500; }
|
||||||
|
|
||||||
/* Indexfelder: rechtsbündiges Label links, Eingabe rechts */
|
/* Indexfelder: rechtsbündiges Label links, Eingabe rechts */
|
||||||
.fields { display: flex; flex-direction: column; gap: 8px; }
|
.fields { display: flex; flex-direction: column; gap: 9px; }
|
||||||
.field { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; }
|
.field { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; }
|
||||||
.field > label { text-align: right; color: #374151; font-weight: 500; padding-top: 2px; }
|
.field > label { text-align: right; color: var(--muted); font-weight: 500; padding-top: 2px; }
|
||||||
.field.full { grid-template-columns: 150px 1fr; align-items: start; }
|
.field.full { grid-template-columns: 150px 1fr; align-items: start; }
|
||||||
.field .req { color: #b91c1c; }
|
.field .req { color: var(--err); }
|
||||||
|
|
||||||
select, input[type="text"], input[type="number"], input[type="date"], textarea {
|
select, input[type="text"], input[type="number"], input[type="date"], input[type="password"], textarea {
|
||||||
width: 100%; padding: 6px 9px; border: 1px solid #b9c0c8; border-radius: 3px; font: inherit; background: #fff;
|
width: 100%; padding: 8px 10px; 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;
|
||||||
}
|
}
|
||||||
select:focus, input:focus, textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px #2563eb33; }
|
select { appearance: none; padding-right: 30px;
|
||||||
textarea { resize: vertical; min-height: 56px; }
|
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;
|
||||||
|
}
|
||||||
|
select:focus, input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
|
||||||
|
input::placeholder { color: #5f6b78; }
|
||||||
|
textarea { resize: vertical; min-height: 60px; }
|
||||||
|
|
||||||
/* Vorbefüllte (aus der Mail übernommene) Felder dezent kennzeichnen */
|
/* Vorbefüllte (aus der Mail übernommene) Felder dezent kennzeichnen */
|
||||||
.field.prefilled input, .field.prefilled textarea { background: #f3f4f6; color: #4b5563; }
|
.field.prefilled input, .field.prefilled textarea, .field.prefilled select {
|
||||||
|
background: rgba(42,164,223,.08); color: #bcc8d3;
|
||||||
|
}
|
||||||
|
|
||||||
.options { margin: 0; }
|
.options { margin: 0; }
|
||||||
.radio, .check { display: flex; align-items: center; gap: 8px; margin: 5px 0; font-weight: 400; }
|
.format { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0; }
|
||||||
.radio input, .check input { accent-color: #2563eb; }
|
.format-label { font-weight: 550; }
|
||||||
|
.format .radio { margin: 0; }
|
||||||
|
.radio, .check { display: flex; align-items: center; gap: 8px; margin: 6px 0; font-weight: 400; }
|
||||||
|
.radio input, .check input { width: 16px; height: 16px; accent-color: var(--accent); flex: none; }
|
||||||
|
|
||||||
.att-list { list-style: none; margin: 6px 0 0 26px; padding: 0; max-height: 110px; overflow-y: auto; }
|
.att-list { list-style: none; margin: 6px 0 0 26px; padding: 0; max-height: 110px; overflow-y: auto; }
|
||||||
.att-list li { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2px 0; color: #374151; }
|
.att-list li { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2px 0; color: var(--muted); }
|
||||||
|
|
||||||
/* Footer bleibt fix sichtbar, nur der Felderbereich (main) scrollt. */
|
/* Footer bleibt fix sichtbar, nur der Felderbereich (main) scrollt. */
|
||||||
footer { border-top: 1px solid #e5e7eb; padding: 10px 18px; background: #fafafa; flex-shrink: 0; }
|
footer { border-top: 1px solid var(--border); padding: 11px 18px; background: var(--surface); flex-shrink: 0; }
|
||||||
.muted { color: #6b7280; font-size: 12px; }
|
.muted { color: var(--muted); font-size: 12px; }
|
||||||
.err { color: #b91c1c; } .ok { color: #15803d; }
|
.err { color: var(--err); } .ok { color: var(--ok); }
|
||||||
.field.invalid input, .field.invalid textarea, .field.invalid select { border-color: #b91c1c; }
|
.field.invalid input, .field.invalid textarea, .field.invalid select { border-color: var(--err); }
|
||||||
|
|
||||||
/* „Bereits abgelegt"-Banner */
|
/* „Bereits abgelegt"-Banner */
|
||||||
.banner {
|
.banner {
|
||||||
display: flex; align-items: center; gap: 10px;
|
display: flex; align-items: center; gap: 10px;
|
||||||
background: #ecfdf5; border: 1px solid #6ee7b7; color: #065f46;
|
background: rgba(70,201,138,.12); border: 1px solid rgba(70,201,138,.4); color: #8be0b4;
|
||||||
border-radius: 4px; padding: 7px 10px; margin-bottom: 10px; font-size: 12px;
|
border-radius: 8px; padding: 8px 11px; margin-bottom: 10px; font-size: 12px;
|
||||||
}
|
}
|
||||||
.banner[hidden] { display: none; }
|
.banner[hidden] { display: none; }
|
||||||
.banner .banner-text { flex: 1; }
|
.banner .banner-text { flex: 1; }
|
||||||
.banner .link { color: #065f46; text-decoration: underline; }
|
.banner .link { color: #8be0b4; text-decoration: underline; }
|
||||||
|
.banner .link:hover { color: var(--ok); }
|
||||||
|
|
||||||
/* Passwort-Abfrage (Sitzungs-Login, nicht gespeichert) */
|
/* Overlays: Passwort-Abfrage / Dubletten-Hinweis */
|
||||||
.overlay {
|
.overlay {
|
||||||
position: fixed; inset: 0; background: rgba(0, 0, 0, .35);
|
position: fixed; inset: 0; background: rgba(0, 0, 0, .55);
|
||||||
display: flex; align-items: center; justify-content: center; z-index: 50;
|
display: flex; align-items: center; justify-content: center; z-index: 50;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
.overlay[hidden] { display: none; }
|
.overlay[hidden] { display: none; }
|
||||||
.overlay-box {
|
.overlay-box {
|
||||||
background: #fff; border-radius: 6px; padding: 18px 20px; width: 340px;
|
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, .25);
|
padding: 20px 22px; width: 350px;
|
||||||
|
box-shadow: 0 18px 50px rgba(0, 0, 0, .5);
|
||||||
}
|
}
|
||||||
.overlay-box h2 { font-size: 15px; font-weight: 600; margin: 0 0 8px; }
|
.overlay-box h2 { font-size: 15px; font-weight: 650; margin: 0 0 8px; }
|
||||||
.overlay-box input[type="password"] {
|
.overlay-box input[type="password"] { margin-top: 8px; }
|
||||||
width: 100%; padding: 7px 9px; border: 1px solid #b9c0c8; border-radius: 3px;
|
|
||||||
font: inherit; margin-top: 8px;
|
|
||||||
}
|
|
||||||
.overlay-box input[type="password"]:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px #2563eb33; }
|
|
||||||
.overlay-box .err { min-height: 1em; margin-top: 6px; }
|
.overlay-box .err { min-height: 1em; margin-top: 6px; }
|
||||||
.overlay-actions { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 10px; }
|
.overlay-actions { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 12px; }
|
||||||
.overlay-box .pw-note { margin-top: 10px; }
|
.overlay-box .pw-note { margin-top: 10px; }
|
||||||
|
|||||||
@ -68,10 +68,12 @@
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<section class="options">
|
<section class="options">
|
||||||
<label class="radio"><input type="radio" name="scope" value="both" checked /> E-Mail und Anhänge</label>
|
<div class="format">
|
||||||
<label class="radio"><input type="radio" name="scope" value="att" /> Nur Anhänge</label>
|
<span class="format-label">E-Mail ablegen als:</span>
|
||||||
<label class="radio"><input type="radio" name="scope" value="eml" /> Nur E-Mail</label>
|
<label class="radio"><input type="radio" name="mailFormat" value="eml" checked /> Original (.eml)</label>
|
||||||
<label class="check"><input type="checkbox" id="optPdf" /> Zusätzlich als PDF ablegen</label>
|
<label class="radio"><input type="radio" name="mailFormat" value="pdf" /> PDF</label>
|
||||||
|
</div>
|
||||||
|
<label class="check"><input type="checkbox" id="optAtt" checked /> Anhänge mit ablegen</label>
|
||||||
<ul id="attList" class="att-list"></ul>
|
<ul id="attList" class="att-list"></ul>
|
||||||
</section>
|
</section>
|
||||||
<div id="status" class="muted"></div>
|
<div id="status" class="muted"></div>
|
||||||
|
|||||||
185
dialog/dialog.js
185
dialog/dialog.js
@ -15,35 +15,45 @@ let STATE = {
|
|||||||
fields: [], // Felddefinitionen des Store-Dialogs
|
fields: [], // Felddefinitionen des Store-Dialogs
|
||||||
existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null)
|
existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null)
|
||||||
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung)
|
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung)
|
||||||
|
uploadedIds: new Set(), // bereits erfolgreich abgelegte Nachrichten (für Retry-Schutz)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mapping DocuWare-Feldname -> Funktion, die einen Vorbefüllwert aus meta liefert.
|
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
|
||||||
const PREFILL = {
|
// Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap).
|
||||||
EML_SENDER: (m) => m.senderEmail,
|
const TOKEN_FN = {
|
||||||
EML_SENDER_DISPLAYNAME: (m) => m.senderName,
|
senderEmail: (m) => m.senderEmail,
|
||||||
EML_SENDER_NAME: (m) => m.senderName,
|
senderName: (m) => m.senderName,
|
||||||
EML_RECEIVER: (m) => m.to,
|
to: (m) => m.to,
|
||||||
EML_CC: (m) => m.cc,
|
cc: (m) => m.cc,
|
||||||
EML_BCC: (m) => m.bcc,
|
bcc: (m) => m.bcc,
|
||||||
EML_SUBJECT: (m) => m.subject,
|
subject: (m) => m.subject,
|
||||||
DOC_SUBJECT: (m) => m.subject,
|
body: (m) => m.bodyText,
|
||||||
EML_BODY: (m) => m.bodyText,
|
date: (m) => m.date,
|
||||||
EML_SENDINGDATE: (m) => m.date,
|
direction: (m) => m.direction,
|
||||||
EML_DISPLAYDATE: (m) => m.date,
|
size: (m) => m.sizeBytes,
|
||||||
EML_RECEIVINGDATE: (m) => m.date,
|
account: (m) => m.account,
|
||||||
EML_DIRECTION: (m) => m.direction,
|
|
||||||
EML_SIZE: (m) => m.sizeBytes,
|
|
||||||
EML_ACCOUNT: (m) => m.account,
|
|
||||||
DOC_DATE: (m) => m.date,
|
|
||||||
// Naheliegende generische Feldnamen (Schränke ohne EML_*-Schema)
|
|
||||||
SUBJECT: (m) => m.subject,
|
|
||||||
BETREFF: (m) => m.subject,
|
|
||||||
EMAIL: (m) => m.senderEmail,
|
|
||||||
E_MAIL: (m) => m.senderEmail,
|
|
||||||
DATE: (m) => m.date,
|
|
||||||
DATUM: (m) => m.date,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default.
|
||||||
|
function effectiveFieldMap() {
|
||||||
|
const m = STATE.settings && STATE.settings.fieldMapping;
|
||||||
|
return m && Object.keys(m).length ? m : DEFAULT_FIELD_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorbefüllwert für ein DocuWare-Feld (oder undefined). Das Mapping kann je Feld
|
||||||
|
// entweder einen Mail-Token (String) ODER einen festen Wert ({ const: "NEU" }) tragen.
|
||||||
|
function prefillFor(fieldName, meta) {
|
||||||
|
const m = effectiveFieldMap()[fieldName];
|
||||||
|
if (!m) return undefined;
|
||||||
|
if (typeof m === "object") {
|
||||||
|
if (m.const !== undefined && m.const !== null && m.const !== "") return m.const;
|
||||||
|
if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const fn = TOKEN_FN[m];
|
||||||
|
return fn ? fn(meta) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function status(msg, kind) {
|
function status(msg, kind) {
|
||||||
const el = $("status");
|
const el = $("status");
|
||||||
el.textContent = msg || "";
|
el.textContent = msg || "";
|
||||||
@ -153,39 +163,51 @@ function askPassword(errMsg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyDefaults() {
|
function applyDefaults() {
|
||||||
$("optPdf").checked = STATE.settings.storePdf;
|
// Mail-Format (.eml ODER PDF) aus den Einstellungen.
|
||||||
|
setMailFormat(STATE.settings.mailFormat === "pdf" ? "pdf" : "eml");
|
||||||
|
// Anhänge-Haken nur sinnvoll, wenn die Mail überhaupt Anhänge hat.
|
||||||
const hasAtt = STATE.attachments.length > 0;
|
const hasAtt = STATE.attachments.length > 0;
|
||||||
// Standard-Umfang aus den Einstellungen ableiten.
|
const att = $("optAtt");
|
||||||
let scope = "both";
|
att.checked = hasAtt && STATE.settings.storeAttachments !== false;
|
||||||
if (STATE.settings.storeEml && !STATE.settings.storeAttachments) scope = "eml";
|
att.disabled = !hasAtt;
|
||||||
else if (!STATE.settings.storeEml && STATE.settings.storeAttachments) scope = "att";
|
$("attList").style.display = hasAtt && att.checked ? "" : "none";
|
||||||
if (!hasAtt) scope = "eml"; // ohne Anhänge nur E-Mail sinnvoll
|
|
||||||
setScope(scope);
|
|
||||||
// Optionen, die Anhänge erfordern, deaktivieren wenn keine vorhanden.
|
|
||||||
document.querySelectorAll('input[name="scope"]').forEach((r) => {
|
|
||||||
if ((r.value === "att" || r.value === "both") && !hasAtt) r.disabled = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setScope(value) {
|
function setMailFormat(value) {
|
||||||
const el = document.querySelector(`input[name="scope"][value="${value}"]`);
|
const el = document.querySelector(`input[name="mailFormat"][value="${value}"]`);
|
||||||
if (el) el.checked = true;
|
if (el) el.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScope() {
|
function getMailFormat() {
|
||||||
const el = document.querySelector('input[name="scope"]:checked');
|
const el = document.querySelector('input[name="mailFormat"]:checked');
|
||||||
return el ? el.value : "both";
|
return el ? el.value : "eml";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAttachments() {
|
function renderAttachments() {
|
||||||
const ul = $("attList");
|
const ul = $("attList");
|
||||||
ul.innerHTML = "";
|
ul.innerHTML = "";
|
||||||
STATE.attachments.forEach((a, i) => {
|
STATE.attachments.forEach((a, i) => {
|
||||||
|
// Bewusst per DOM-API (nicht innerHTML): der Anhang-Name stammt aus der Mail
|
||||||
|
// und ist angreiferkontrolliert – als Text einsetzen, nie als HTML.
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.innerHTML =
|
|
||||||
`<input type="checkbox" id="att-${i}" checked /> ` +
|
const cb = document.createElement("input");
|
||||||
`<label for="att-${i}" style="margin:0;font-weight:400">${a.name} ` +
|
cb.type = "checkbox";
|
||||||
`<span class="muted">(${formatSize(a.size)})</span></label>`;
|
cb.id = `att-${i}`;
|
||||||
|
cb.checked = true;
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.setAttribute("for", `att-${i}`);
|
||||||
|
label.style.margin = "0";
|
||||||
|
label.style.fontWeight = "400";
|
||||||
|
label.textContent = `${a.name} `;
|
||||||
|
|
||||||
|
const size = document.createElement("span");
|
||||||
|
size.className = "muted";
|
||||||
|
size.textContent = `(${formatSize(a.size)})`;
|
||||||
|
label.appendChild(size);
|
||||||
|
|
||||||
|
li.append(cb, document.createTextNode(" "), label);
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -424,7 +446,8 @@ function buildInput(f) {
|
|||||||
if (f.isSelectList) {
|
if (f.isSelectList) {
|
||||||
// Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook).
|
// Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook).
|
||||||
const dl = document.createElement("datalist");
|
const dl = document.createElement("datalist");
|
||||||
dl.id = "dl-" + f.name;
|
// Feldnamen können Leer-/Sonderzeichen enthalten -> auf gültige id-Zeichen reduzieren.
|
||||||
|
dl.id = "dl-" + String(f.name).replace(/[^A-Za-z0-9_-]/g, "_");
|
||||||
el.setAttribute("list", dl.id);
|
el.setAttribute("list", dl.id);
|
||||||
el._datalist = dl;
|
el._datalist = dl;
|
||||||
}
|
}
|
||||||
@ -444,9 +467,7 @@ function applyFieldDefaults() {
|
|||||||
|
|
||||||
function prefillFields() {
|
function prefillFields() {
|
||||||
STATE.fields.forEach((f) => {
|
STATE.fields.forEach((f) => {
|
||||||
const fn = PREFILL[f.name];
|
const val = prefillFor(f.name, STATE.meta);
|
||||||
if (!fn) return;
|
|
||||||
const val = fn(STATE.meta);
|
|
||||||
if (val === undefined || val === null || val === "") return;
|
if (val === undefined || val === null || val === "") return;
|
||||||
setFieldValue(f.name, f, val);
|
setFieldValue(f.name, f, val);
|
||||||
});
|
});
|
||||||
@ -454,7 +475,7 @@ function prefillFields() {
|
|||||||
|
|
||||||
// Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select).
|
// Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select).
|
||||||
function setFieldValue(name, f, val) {
|
function setFieldValue(name, f, val) {
|
||||||
const wrap = document.querySelector(`.field[data-name="${name}"]`);
|
const wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`);
|
||||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (el.type === "date") {
|
if (el.type === "date") {
|
||||||
@ -485,7 +506,7 @@ async function populateSelectLists() {
|
|||||||
);
|
);
|
||||||
} catch (_) { /* leere Liste ist ok */ }
|
} catch (_) { /* leere Liste ist ok */ }
|
||||||
if (!vals.length) continue;
|
if (!vals.length) continue;
|
||||||
const wrap = document.querySelector(`.field[data-name="${f.name}"]`);
|
const wrap = document.querySelector(`.field[data-name="${cssEsc(f.name)}"]`);
|
||||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||||
if (!el) continue;
|
if (!el) continue;
|
||||||
if (el.tagName === "SELECT") {
|
if (el.tagName === "SELECT") {
|
||||||
@ -527,7 +548,10 @@ function collectFieldValues() {
|
|||||||
if (!firstInvalid) firstInvalid = name;
|
if (!firstInvalid) firstInvalid = name;
|
||||||
}
|
}
|
||||||
if (String(raw).trim() !== "") {
|
if (String(raw).trim() !== "") {
|
||||||
result[name] = { value: el.type === "date" ? new Date(raw) : raw, type };
|
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
|
||||||
|
// interpretiert new Date("YYYY-MM-DD") als UTC.
|
||||||
|
const value = el.type === "date" ? new Date(`${raw}T00:00:00`) : raw;
|
||||||
|
result[name] = { value, type };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { result, firstInvalid };
|
return { result, firstInvalid };
|
||||||
@ -545,15 +569,11 @@ async function submit() {
|
|||||||
result = collected.result;
|
result = collected.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = getScope();
|
const fmt = getMailFormat(); // "eml" ODER "pdf" – nie beides
|
||||||
const wantEml = scope !== "att";
|
const wantEml = fmt === "eml";
|
||||||
const wantPdf = $("optPdf").checked;
|
const wantPdf = fmt === "pdf";
|
||||||
const wantAttScope = scope !== "eml";
|
const att = $("optAtt");
|
||||||
|
const wantAttScope = att.checked && !att.disabled;
|
||||||
if (!wantEml && !wantAttScope) {
|
|
||||||
status("Nichts ausgewählt zum Ablegen.", "err");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dubletten-Warnung (nur Einzelmail): liegt die Mail im Ziel schon, warnen –
|
// Dubletten-Warnung (nur Einzelmail): liegt die Mail im Ziel schon, warnen –
|
||||||
// „Trotzdem ablegen" bleibt aber möglich.
|
// „Trotzdem ablegen" bleibt aber möglich.
|
||||||
@ -575,7 +595,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveOptionPrefs(scope, wantPdf); // Sticky: Umfang merken
|
await saveOptionPrefs(fmt, wantAttScope); // Sticky: Format + Anhänge-Wahl merken
|
||||||
|
|
||||||
$("submit").disabled = true;
|
$("submit").disabled = true;
|
||||||
const multi = STATE.messageIds.length > 1;
|
const multi = STATE.messageIds.length > 1;
|
||||||
@ -585,6 +605,8 @@ async function submit() {
|
|||||||
let mi = 0;
|
let mi = 0;
|
||||||
for (const mid of STATE.messageIds) {
|
for (const mid of STATE.messageIds) {
|
||||||
mi++;
|
mi++;
|
||||||
|
// Retry-Schutz: nach einem Teilfehler nicht erneut hochladen (sonst Dubletten).
|
||||||
|
if (STATE.uploadedIds.has(mid)) continue;
|
||||||
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
|
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
|
||||||
// Meta/Anhänge der jeweiligen Nachricht.
|
// Meta/Anhänge der jeweiligen Nachricht.
|
||||||
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid);
|
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid);
|
||||||
@ -633,6 +655,9 @@ async function submit() {
|
|||||||
.sendMessage({ type: "archive:done", rfcMessageId: meta.rfcMessageId, cabinetId: STATE.cabinetId })
|
.sendMessage({ type: "archive:done", rfcMessageId: meta.rfcMessageId, cabinetId: STATE.cabinetId })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diese Nachricht ist vollständig abgelegt -> bei einem späteren Retry überspringen.
|
||||||
|
STATE.uploadedIds.add(mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
|
const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
|
||||||
@ -650,21 +675,17 @@ function fieldsForMessage(baseResult, meta, multi) {
|
|||||||
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
|
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
|
||||||
const result = { ...baseResult };
|
const result = { ...baseResult };
|
||||||
STATE.fields.forEach((f) => {
|
STATE.fields.forEach((f) => {
|
||||||
const fn = PREFILL[f.name];
|
const val = prefillFor(f.name, meta);
|
||||||
if (!fn) return;
|
|
||||||
const val = fn(meta);
|
|
||||||
if (val === undefined || val === null || val === "") return;
|
if (val === undefined || val === null || val === "") return;
|
||||||
result[f.name] = { value: val, type: f.type };
|
result[f.name] = { value: val, type: f.type };
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sticky: zuletzt genutzten Ablage-Umfang in den Einstellungen merken.
|
// Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken.
|
||||||
async function saveOptionPrefs(scope, wantPdf) {
|
async function saveOptionPrefs(mailFormat, storeAttachments) {
|
||||||
const storeEml = scope !== "att";
|
|
||||||
const storeAttachments = scope !== "eml";
|
|
||||||
try {
|
try {
|
||||||
STATE.settings = await Settings.set({ storeEml, storeAttachments, storePdf: wantPdf });
|
STATE.settings = await Settings.set({ mailFormat, storeAttachments });
|
||||||
} catch (_) { /* nicht kritisch */ }
|
} catch (_) { /* nicht kritisch */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -700,7 +721,17 @@ async function tagMessage(messageId) {
|
|||||||
|
|
||||||
// --- Helfer ---
|
// --- Helfer ---
|
||||||
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY";
|
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY";
|
||||||
const toDateInput = (d) => d.toISOString().slice(0, 10);
|
// Lokales Datum als YYYY-MM-DD (NICHT toISOString -> das rechnet auf UTC um und
|
||||||
|
// kippt eine Uhrzeit nahe Mitternacht auf den Vortag).
|
||||||
|
const toDateInput = (d) => {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
};
|
||||||
|
// Sicheres CSS-Escaping für Attribut-Selektoren (Feldnamen aus DocuWare).
|
||||||
|
const cssEsc = (s) =>
|
||||||
|
window.CSS && CSS.escape ? CSS.escape(String(s)) : String(s).replace(/["\\\]]/g, "\\$&");
|
||||||
function formatSize(b) {
|
function formatSize(b) {
|
||||||
if (!b) return "–";
|
if (!b) return "–";
|
||||||
if (b < 1024) return `${b} B`;
|
if (b < 1024) return `${b} B`;
|
||||||
@ -728,11 +759,13 @@ $("reset").addEventListener("click", resetForm);
|
|||||||
$("cancel").addEventListener("click", () => window.close());
|
$("cancel").addEventListener("click", () => window.close());
|
||||||
|
|
||||||
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
|
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
|
||||||
document.querySelectorAll('input[name="scope"]').forEach((r) =>
|
document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
|
||||||
r.addEventListener("change", () => saveOptionPrefs(getScope(), $("optPdf").checked))
|
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
|
||||||
);
|
|
||||||
$("optPdf").addEventListener("change", () =>
|
|
||||||
saveOptionPrefs(getScope(), $("optPdf").checked)
|
|
||||||
);
|
);
|
||||||
|
$("optAtt").addEventListener("change", () => {
|
||||||
|
const show = $("optAtt").checked && STATE.attachments.length > 0;
|
||||||
|
$("attList").style.display = show ? "" : "none";
|
||||||
|
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
|
||||||
|
});
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@ -414,7 +414,12 @@ const DocuWare = {
|
|||||||
if (t.includes("date")) {
|
if (t.includes("date")) {
|
||||||
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
|
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
|
||||||
const d = value instanceof Date ? value : new Date(value);
|
const d = value instanceof Date ? value : new Date(value);
|
||||||
return { FieldName: name, Item: d.toISOString(), ItemElementName: "Date" };
|
if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "Date" };
|
||||||
|
// Auf den LOKALEN Kalendertag als UTC-Mitternacht normalisieren. Sonst würde
|
||||||
|
// toISOString eine Uhrzeit nahe Mitternacht in eine andere Zeitzone und damit
|
||||||
|
// auf den Vor-/Folgetag kippen (Datumsfelder tragen keine Uhrzeit).
|
||||||
|
const utcDay = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||||||
|
return { FieldName: name, Item: utcDay.toISOString(), ItemElementName: "Date" };
|
||||||
}
|
}
|
||||||
if (t.includes("decimal") || t.includes("currency")) {
|
if (t.includes("decimal") || t.includes("currency")) {
|
||||||
return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" };
|
return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" };
|
||||||
|
|||||||
@ -29,7 +29,7 @@ const Mail = {
|
|||||||
date: header.date instanceof Date ? header.date : new Date(header.date),
|
date: header.date instanceof Date ? header.date : new Date(header.date),
|
||||||
bodyText,
|
bodyText,
|
||||||
account,
|
account,
|
||||||
direction: this._direction(header, account),
|
direction: this._direction(header),
|
||||||
sizeBytes: header.size || 0,
|
sizeBytes: header.size || 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -140,9 +140,8 @@ const Mail = {
|
|||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
_direction(header, accountName) {
|
_direction(header) {
|
||||||
// Heuristik: ist der Account-Name/eine Identität Absender -> Ausgang.
|
// Heuristik über den Ordnertyp; robustere Erkennung via Identitäten käme später.
|
||||||
// Robustere Erkennung erfolgt später über Identitäten; v1 simpel:
|
|
||||||
const folderType = header.folder && header.folder.type;
|
const folderType = header.folder && header.folder.type;
|
||||||
if (folderType === "sent" || folderType === "outbox") return "Ausgang";
|
if (folderType === "sent" || folderType === "outbox") return "Ausgang";
|
||||||
return "Eingang";
|
return "Eingang";
|
||||||
|
|||||||
71
lib/store.js
71
lib/store.js
@ -9,25 +9,87 @@ const DEFAULTS = {
|
|||||||
username: "",
|
username: "",
|
||||||
defaultCabinetId: "",
|
defaultCabinetId: "",
|
||||||
dialogByCabinet: {}, // gemerkter Ablagedialog je Schrank: { [cabinetId]: dialogId }
|
dialogByCabinet: {}, // gemerkter Ablagedialog je Schrank: { [cabinetId]: dialogId }
|
||||||
storeEml: true,
|
mailFormat: "eml", // E-Mail wird als "eml" ODER "pdf" abgelegt (nie beides)
|
||||||
storePdf: true,
|
|
||||||
storeAttachments: true,
|
storeAttachments: true,
|
||||||
tagOnSuccess: true,
|
tagOnSuccess: true,
|
||||||
|
// DocuWare-Feldname -> Wert: entweder ein Mail-Token ("subject") ODER ein fester
|
||||||
|
// Wert ({ const: "NEU" }). Leer = DEFAULT_FIELD_MAP.
|
||||||
|
fieldMapping: {},
|
||||||
|
mappingUrl: "", // optional: raw-JSON-URL (Gitea/GitHub) zum Nachladen des Mappings.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mögliche Mail-Attribute für die Vorbefüllung (Token + Anzeigename).
|
||||||
|
// Wird von dialog.js (Werte aus meta) und options.js (Mapping-Editor) genutzt.
|
||||||
|
const MAIL_TOKENS = [
|
||||||
|
{ key: "senderEmail", label: "Absender – E-Mail" },
|
||||||
|
{ key: "senderName", label: "Absender – Name" },
|
||||||
|
{ key: "to", label: "Empfänger (An)" },
|
||||||
|
{ key: "cc", label: "CC" },
|
||||||
|
{ key: "bcc", label: "BCC" },
|
||||||
|
{ key: "subject", label: "Betreff" },
|
||||||
|
{ key: "body", label: "Text (Body)" },
|
||||||
|
{ key: "date", label: "Datum" },
|
||||||
|
{ key: "direction", label: "Richtung (Ein-/Ausgang)" },
|
||||||
|
{ key: "size", label: "Größe (Bytes)" },
|
||||||
|
{ key: "account", label: "Konto" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Standard-Zuordnung DocuWare-Feldname -> Mail-Token. Greift, solange der Nutzer
|
||||||
|
// in den Einstellungen kein eigenes fieldMapping gespeichert hat.
|
||||||
|
const DEFAULT_FIELD_MAP = {
|
||||||
|
EML_SENDER: "senderEmail",
|
||||||
|
EML_SENDER_DISPLAYNAME: "senderName",
|
||||||
|
EML_SENDER_NAME: "senderName",
|
||||||
|
EML_RECEIVER: "to",
|
||||||
|
EML_CC: "cc",
|
||||||
|
EML_BCC: "bcc",
|
||||||
|
EML_SUBJECT: "subject",
|
||||||
|
DOC_SUBJECT: "subject",
|
||||||
|
EML_BODY: "body",
|
||||||
|
EML_SENDINGDATE: "date",
|
||||||
|
EML_DISPLAYDATE: "date",
|
||||||
|
EML_RECEIVINGDATE: "date",
|
||||||
|
EML_DIRECTION: "direction",
|
||||||
|
EML_SIZE: "size",
|
||||||
|
EML_ACCOUNT: "account",
|
||||||
|
DOC_DATE: "date",
|
||||||
|
SUBJECT: "subject",
|
||||||
|
BETREFF: "subject",
|
||||||
|
EMAIL: "senderEmail",
|
||||||
|
E_MAIL: "senderEmail",
|
||||||
|
DATE: "date",
|
||||||
|
DATUM: "date",
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings = {
|
const Settings = {
|
||||||
async get() {
|
async get() {
|
||||||
const stored = await browser.storage.local.get("settings");
|
const stored = await browser.storage.local.get("settings");
|
||||||
return { ...DEFAULTS, ...(stored.settings || {}) };
|
const raw = stored.settings || {};
|
||||||
|
const s = { ...DEFAULTS, ...raw };
|
||||||
|
// Migration: altes storeEml/storePdf -> mailFormat (einmalig, nicht persistiert).
|
||||||
|
if (raw.mailFormat === undefined && raw.storePdf && !raw.storeEml) {
|
||||||
|
s.mailFormat = "pdf";
|
||||||
|
}
|
||||||
|
return s;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Laufende Schreibkette: serialisiert parallele set()-Aufrufe, damit zwei fast
|
||||||
|
// gleichzeitige Writes (z.B. Schrankwahl + Format-Umschalten) sich nicht beim
|
||||||
|
// read-modify-write gegenseitig überschreiben.
|
||||||
|
_writeQueue: Promise.resolve(),
|
||||||
|
|
||||||
async set(partial) {
|
async set(partial) {
|
||||||
|
const run = async () => {
|
||||||
const current = await this.get();
|
const current = await this.get();
|
||||||
const next = { ...current, ...partial };
|
const next = { ...current, ...partial };
|
||||||
// Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen).
|
// Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen).
|
||||||
delete next.password;
|
delete next.password;
|
||||||
await browser.storage.local.set({ settings: next });
|
await browser.storage.local.set({ settings: next });
|
||||||
return next;
|
return next;
|
||||||
|
};
|
||||||
|
const p = this._writeQueue.then(run, run);
|
||||||
|
this._writeQueue = p.catch(() => {}); // Fehler bricht die Kette nicht ab
|
||||||
|
return p;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Basis-URL der Platform-API ohne abschließenden Slash. */
|
/** Basis-URL der Platform-API ohne abschließenden Slash. */
|
||||||
@ -38,4 +100,5 @@ const Settings = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof module !== "undefined") module.exports = { Settings, DEFAULTS };
|
if (typeof module !== "undefined")
|
||||||
|
module.exports = { Settings, DEFAULTS, MAIL_TOKENS, DEFAULT_FIELD_MAP };
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "DocuWare Ablage",
|
"name": "DocuWare Ablage",
|
||||||
"description": "Legt markierte E-Mails (als .eml, PDF und mit Anhängen) in DocuWare ab.",
|
"description": "Legt markierte E-Mails (als .eml, PDF und mit Anhängen) in DocuWare ab.",
|
||||||
"version": "0.10.2",
|
"version": "0.10.8",
|
||||||
"author": "l.lingler",
|
"author": "l.lingler",
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@ -4,26 +4,178 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>DocuWare Ablage – Einstellungen</title>
|
<title>DocuWare Ablage – Einstellungen</title>
|
||||||
<style>
|
<style>
|
||||||
body { font: 14px/1.5 system-ui, sans-serif; max-width: 560px; margin: 24px auto; padding: 0 16px; color: #1a1a1a; }
|
/* DocuWare-naher Dark-Style: tiefes Slate, DocuWare-Blau als Akzent. */
|
||||||
h1 { font-size: 20px; }
|
:root {
|
||||||
fieldset { border: 1px solid #ddd; border-radius: 8px; margin: 16px 0; padding: 12px 16px; }
|
--bg: #11151b;
|
||||||
legend { font-weight: 600; padding: 0 6px; }
|
--bg-grad: radial-gradient(1200px 500px at 80% -10%, #18324a 0%, rgba(17,21,27,0) 60%);
|
||||||
label { display: block; margin: 10px 0 4px; font-weight: 500; }
|
--surface: #1a1f27;
|
||||||
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; }
|
--surface-2: #222a34;
|
||||||
.check { display: flex; align-items: center; gap: 8px; font-weight: 400; margin: 8px 0; }
|
--border: #2b3340;
|
||||||
.row { display: flex; gap: 12px; align-items: center; margin-top: 16px; }
|
--border-strong: #38424f;
|
||||||
button { padding: 8px 16px; border: 0; border-radius: 6px; background: #2563eb; color: #fff; font-weight: 600; cursor: pointer; }
|
--text: #e8edf2;
|
||||||
button.secondary { background: #e5e7eb; color: #111; }
|
--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; }
|
||||||
|
/* Mapping-Zeilen: Feldname + Attribut links gestapelt, fester Wert breit daneben. */
|
||||||
|
.map { margin-top: 4px; }
|
||||||
|
.map-row {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
background: var(--surface-2); border: 1px solid var(--border);
|
||||||
|
border-radius: 9px; padding: 8px 8px 8px 10px; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.map-left { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; min-width: 0; }
|
||||||
|
.map-row.has-const .map-left { flex: 0 0 250px; }
|
||||||
|
.map-right { display: none; flex: 1 1 auto; min-width: 0; }
|
||||||
|
.map-row.has-const .map-right { display: flex; align-items: center; }
|
||||||
|
.map-left select, .map-right input { width: 100%; padding: 7px 9px; }
|
||||||
|
.map-hint { font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
|
||||||
|
color: var(--muted); font-weight: 600; margin: 2px 2px 8px; }
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
/* Teilen / JSON */
|
||||||
|
details.share { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 10px; }
|
||||||
|
details.share summary {
|
||||||
|
cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent);
|
||||||
|
list-style: none; user-select: none; padding: 2px 0;
|
||||||
|
}
|
||||||
|
details.share summary::-webkit-details-marker { display: none; }
|
||||||
|
details.share summary::before { content: "▸ "; color: var(--muted); }
|
||||||
|
details.share[open] summary::before { content: "▾ "; }
|
||||||
|
textarea {
|
||||||
|
width: 100%; padding: 10px 11px; color: var(--text); background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border-strong); border-radius: 8px; box-sizing: border-box;
|
||||||
|
font: 12px/1.45 ui-monospace, "Cascadia Code", Consolas, monospace; resize: vertical; outline: none;
|
||||||
|
}
|
||||||
|
textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-weak); }
|
||||||
|
|
||||||
|
/* 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; }
|
#status { font-size: 13px; min-height: 18px; }
|
||||||
.ok { color: #15803d; } .err { color: #b91c1c; }
|
.ok { color: var(--ok); } .err { color: var(--err); }
|
||||||
.hint { font-size: 12px; color: #666; margin-top: 2px; }
|
.spacer { flex: 1; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<section class="card">
|
||||||
<legend>Verbindung</legend>
|
<div class="card-head"><span class="num">1</span><h2>Verbindung</h2></div>
|
||||||
<label for="serverUrl">Server-URL</label>
|
<label for="serverUrl">Server-URL</label>
|
||||||
<input type="url" id="serverUrl" placeholder="https://docuware.example.com" />
|
<input type="url" id="serverUrl" placeholder="https://docuware.example.com" />
|
||||||
<div class="hint">Basis-URL ohne <code>/DocuWare/Platform</code>.</div>
|
<div class="hint">Basis-URL ohne <code>/DocuWare/Platform</code>.</div>
|
||||||
@ -38,50 +190,76 @@
|
|||||||
<input type="password" id="password" autocomplete="off" />
|
<input type="password" id="password" autocomplete="off" />
|
||||||
<div class="hint">Wird <strong>nicht gespeichert</strong> – nur zum Anmelden/Testen für
|
<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>
|
diese Sitzung. Im Ablage-Fenster wird es 1× pro Sitzung abgefragt.</div>
|
||||||
</fieldset>
|
</section>
|
||||||
|
|
||||||
<fieldset>
|
<section class="card">
|
||||||
<legend>Standardwerte</legend>
|
<div class="card-head"><span class="num">2</span><h2>Standardwerte</h2></div>
|
||||||
<label for="defaultCabinet">Standard-Ziel (Archiv oder Briefkorb)</label>
|
<label for="defaultCabinet">Standard-Ziel (Archiv oder Briefkorb)</label>
|
||||||
<select id="defaultCabinet" style="width:100%;padding:7px;border:1px solid #ccc;border-radius:6px;">
|
<select id="defaultCabinet"><option value="">— keiner —</option></select>
|
||||||
<option value="">— keiner —</option>
|
|
||||||
</select>
|
|
||||||
<div class="hint">Liste wird nach erfolgreichem Verbindungstest gefüllt.</div>
|
<div class="hint">Liste wird nach erfolgreichem Verbindungstest gefüllt.</div>
|
||||||
|
|
||||||
<div class="check"><input type="checkbox" id="storeEml" /> <label for="storeEml" style="margin:0">E-Mail als .eml ablegen</label></div>
|
<label>Standard-Format der E-Mail</label>
|
||||||
<div class="check"><input type="checkbox" id="storePdf" /> <label for="storePdf" style="margin:0">E-Mail als PDF ablegen</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="checkbox" id="storeAttachments" /> <label for="storeAttachments" style="margin:0">Anhänge separat ablegen</label></div>
|
<div class="check"><input type="radio" name="mailFormat" id="fmtPdf" value="pdf" /> <label for="fmtPdf">PDF</label></div>
|
||||||
<div class="check"><input type="checkbox" id="tagOnSuccess" /> <label for="tagOnSuccess" style="margin:0">Mail nach Ablage als „DocuWare" markieren</label></div>
|
<div class="hint">Die E-Mail wird immer in genau einem Format abgelegt (nicht beides). Im Ablage-Fenster umstellbar.</div>
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="check" style="margin-top:12px"><input type="checkbox" id="storeAttachments" /> <label for="storeAttachments">Anhänge mit ablegen</label></div>
|
||||||
<button id="save">Speichern</button>
|
<div class="check"><input type="checkbox" id="tagOnSuccess" /> <label for="tagOnSuccess">Mail nach Ablage als „DocuWare" markieren</label></div>
|
||||||
<button id="test" class="secondary">Verbindung testen</button>
|
</section>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="map-hint">DocuWare-Feld → Mail-Attribut · fester Wert</div>
|
||||||
|
<div id="mapBody" class="map"></div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div class="hint" style="margin-top:8px">Als Mail-Attribut „<strong>Fester Wert</strong>"
|
||||||
|
wählen, um einen festen Text (z. B. <code>NEU</code>) ins Feld zu schreiben.</div>
|
||||||
|
|
||||||
|
<details class="share">
|
||||||
|
<summary>Mapping teilen / übernehmen</summary>
|
||||||
|
|
||||||
|
<label for="mappingUrl">Aus Repo/URL laden</label>
|
||||||
|
<div class="map-toolbar">
|
||||||
|
<input type="url" id="mappingUrl" class="grow" placeholder="https://gitea.example/…/mapping.json" />
|
||||||
|
<button id="mappingPull" class="ghost" type="button">Von URL laden</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">Eine raw-JSON-Datei aus einem Gitea/GitHub-Repo. Lädt das Mapping in
|
||||||
|
den Editor – mit „Speichern" übernehmen. (Lesen/Pull; Zurückschreiben ins Repo nicht.)</div>
|
||||||
|
|
||||||
|
<label for="mappingJson">JSON (kopieren / einfügen)</label>
|
||||||
|
<textarea id="mappingJson" rows="6" placeholder='{ "EML_SUBJECT": "subject", "STATUS": { "const": "NEU" } }'></textarea>
|
||||||
|
<div class="map-toolbar">
|
||||||
|
<button id="mappingExport" class="ghost" type="button">Aktuelles als JSON</button>
|
||||||
|
<button id="mappingCopy" class="ghost" type="button">Kopieren</button>
|
||||||
|
<button id="mappingApply" class="ghost" type="button">JSON anwenden</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
<span id="status"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
</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>
|
|
||||||
<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/store.js"></script>
|
||||||
<script src="../lib/auth.js"></script>
|
<script src="../lib/auth.js"></script>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const $ = (id) => document.getElementById(id);
|
|||||||
// Passwort ist bewusst NICHT dabei – es wird nie gespeichert, nur zum Testen/Login
|
// Passwort ist bewusst NICHT dabei – es wird nie gespeichert, nur zum Testen/Login
|
||||||
// transient verwendet (siehe "Verbindung testen").
|
// transient verwendet (siehe "Verbindung testen").
|
||||||
const FIELDS = ["serverUrl", "organization", "username"];
|
const FIELDS = ["serverUrl", "organization", "username"];
|
||||||
const CHECKS = ["storeEml", "storePdf", "storeAttachments", "tagOnSuccess"];
|
const CHECKS = ["storeAttachments", "tagOnSuccess"];
|
||||||
|
|
||||||
function setStatus(msg, kind) {
|
function setStatus(msg, kind) {
|
||||||
const el = $("status");
|
const el = $("status");
|
||||||
@ -12,16 +12,53 @@ function setStatus(msg, kind) {
|
|||||||
el.className = kind || "";
|
el.className = kind || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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."
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const s = await Settings.get();
|
const s = await Settings.get();
|
||||||
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
|
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
|
||||||
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
|
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
|
||||||
|
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
|
||||||
|
$("mappingUrl").value = s.mappingUrl || "";
|
||||||
|
loadMapping(s.fieldMapping && Object.keys(s.fieldMapping).length ? s.fieldMapping : DEFAULT_FIELD_MAP);
|
||||||
// Schrankliste, falls vorher schon getestet (im Storage gecached)
|
// Schrankliste, falls vorher schon getestet (im Storage gecached)
|
||||||
const cached = (await browser.storage.local.get("cabinets")).cabinets || [];
|
const cached = (await browser.storage.local.get("cabinets")).cabinets || [];
|
||||||
fillCabinets(cached, s.defaultCabinetId);
|
fillCabinets(cached, s.defaultCabinetId);
|
||||||
|
|
||||||
|
// Best-effort: Felder des Standard-Archivs gleich laden – nur wenn schon ein
|
||||||
|
// Sitzungs-Token da ist (sonst wählt der Nutzer das Archiv manuell).
|
||||||
|
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();
|
||||||
|
refreshFieldSelects();
|
||||||
|
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
|
||||||
|
}
|
||||||
|
} catch (_) { /* egal – Archiv kann manuell gewählt werden */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillCabinets(cabinets, selectedId) {
|
function fillCabinets(cabinets, selectedId) {
|
||||||
|
// Standard-Ziel (Archive + Briefkörbe)
|
||||||
const sel = $("defaultCabinet");
|
const sel = $("defaultCabinet");
|
||||||
sel.innerHTML = '<option value="">— keiner —</option>';
|
sel.innerHTML = '<option value="">— keiner —</option>';
|
||||||
const addGroup = (label, list) => {
|
const addGroup = (label, list) => {
|
||||||
@ -39,6 +76,135 @@ function fillCabinets(cabinets, selectedId) {
|
|||||||
};
|
};
|
||||||
addGroup("Archive", cabinets.filter((c) => !c.isBasket));
|
addGroup("Archive", cabinets.filter((c) => !c.isBasket));
|
||||||
addGroup("Briefkörbe", 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 ---------------------------------------------------
|
||||||
|
|
||||||
|
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
|
||||||
|
let availableFields = []; // Feldnamen aus dem zuletzt gewählten Archiv
|
||||||
|
|
||||||
|
// 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: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Befüllt ein Feldname-Dropdown mit den Archiv-Feldern + dem aktuellen Wert (bleibt erhalten).
|
||||||
|
function fillFieldSelect(sel, current) {
|
||||||
|
sel.innerHTML = "";
|
||||||
|
const empty = document.createElement("option");
|
||||||
|
empty.value = ""; empty.textContent = "— Feld wählen —";
|
||||||
|
sel.appendChild(empty);
|
||||||
|
[...new Set([...(availableFields || []), current].filter(Boolean))].sort().forEach((n) => {
|
||||||
|
const o = document.createElement("option");
|
||||||
|
o.value = n; o.textContent = n;
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
sel.value = current || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisiert alle Feldname-Dropdowns auf die aktuelle Archiv-Feldliste (Auswahl bleibt).
|
||||||
|
function refreshFieldSelects() {
|
||||||
|
document.querySelectorAll("#mapBody .map-field").forEach((sel) => fillFieldSelect(sel, sel.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMapRow(field = "", value = "") {
|
||||||
|
const st = mapValueState(value);
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : "");
|
||||||
|
|
||||||
|
// Links gestapelt: Feldname (Dropdown) über Mail-Attribut.
|
||||||
|
const left = document.createElement("div"); left.className = "map-left";
|
||||||
|
|
||||||
|
const fieldSel = document.createElement("select"); fieldSel.className = "map-field";
|
||||||
|
fillFieldSelect(fieldSel, field);
|
||||||
|
|
||||||
|
const src = document.createElement("select"); src.className = "map-source";
|
||||||
|
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
|
||||||
|
src.appendChild(none);
|
||||||
|
MAIL_TOKENS.forEach((t) => {
|
||||||
|
const o = document.createElement("option");
|
||||||
|
o.value = t.key; o.textContent = t.label;
|
||||||
|
if (t.key === st.sel) o.selected = true;
|
||||||
|
src.appendChild(o);
|
||||||
|
});
|
||||||
|
const co = document.createElement("option");
|
||||||
|
co.value = CONST_OPT; co.textContent = "★ Fester Wert";
|
||||||
|
if (st.sel === CONST_OPT) co.selected = true;
|
||||||
|
src.appendChild(co);
|
||||||
|
|
||||||
|
left.append(fieldSel, src);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
const del = document.createElement("button");
|
||||||
|
del.type = "button"; del.className = "icon-btn"; del.textContent = "✕"; del.title = "Zeile entfernen";
|
||||||
|
del.addEventListener("click", () => row.remove());
|
||||||
|
|
||||||
|
row.append(left, right, del);
|
||||||
|
$("mapBody").appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMapping(map) {
|
||||||
|
$("mapBody").innerHTML = "";
|
||||||
|
Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMapping() {
|
||||||
|
const map = {};
|
||||||
|
document.querySelectorAll("#mapBody .map-row").forEach((row) => {
|
||||||
|
const f = row.querySelector(".map-field").value.trim();
|
||||||
|
if (!f) return; // ohne Feldname ignorieren
|
||||||
|
const sel = row.querySelector(".map-source").value;
|
||||||
|
if (sel === CONST_OPT) {
|
||||||
|
const c = row.querySelector(".map-const").value;
|
||||||
|
if (c !== "") map[f] = { const: c }; // fester Wert
|
||||||
|
} else if (sel) {
|
||||||
|
map[f] = sel; // Mail-Token (als String, abwärtskompatibel)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collect() {
|
function collect() {
|
||||||
@ -46,96 +212,106 @@ function collect() {
|
|||||||
FIELDS.forEach((f) => (partial[f] = $(f).value.trim()));
|
FIELDS.forEach((f) => (partial[f] = $(f).value.trim()));
|
||||||
CHECKS.forEach((c) => (partial[c] = $(c).checked));
|
CHECKS.forEach((c) => (partial[c] = $(c).checked));
|
||||||
partial.defaultCabinetId = $("defaultCabinet").value;
|
partial.defaultCabinetId = $("defaultCabinet").value;
|
||||||
|
partial.mailFormat = getMailFormat();
|
||||||
|
partial.fieldMapping = collectMapping();
|
||||||
|
partial.mappingUrl = $("mappingUrl").value.trim();
|
||||||
return partial;
|
return partial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Events ----------------------------------------------------------------
|
||||||
|
|
||||||
$("save").addEventListener("click", async () => {
|
$("save").addEventListener("click", async () => {
|
||||||
await Settings.set(collect());
|
const data = collect();
|
||||||
setStatus("Gespeichert.", "ok");
|
await Settings.set(data);
|
||||||
|
const warn = insecureWarning(data.serverUrl);
|
||||||
|
setStatus("Gespeichert." + warn, warn ? "err" : "ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("test").addEventListener("click", async () => {
|
$("test").addEventListener("click", async () => {
|
||||||
const pw = $("password").value;
|
|
||||||
if (!pw) { setStatus("Bitte Passwort eingeben.", "err"); return; }
|
|
||||||
setStatus("Verbinde …");
|
setStatus("Verbinde …");
|
||||||
try {
|
try {
|
||||||
const s = await Settings.set(collect()); // erst speichern (ohne Passwort), dann testen
|
const s = await Settings.set(collect()); // erst speichern (ohne Passwort), dann testen
|
||||||
// Anmeldung über den Hintergrund-Broker: primt zugleich die Sitzung, sodass
|
const ok = await ensureToken();
|
||||||
// das Ablege-Fenster danach nicht erneut nach dem Passwort fragt.
|
if (!ok) { setStatus("Bitte Passwort eingeben.", "err"); 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 = ""; // Passwort nicht behalten
|
|
||||||
const cabinets = await DocuWare.listCabinets(s);
|
const cabinets = await DocuWare.listCabinets(s);
|
||||||
await browser.storage.local.set({ cabinets });
|
await browser.storage.local.set({ cabinets });
|
||||||
fillCabinets(cabinets, s.defaultCabinetId);
|
fillCabinets(cabinets, s.defaultCabinetId);
|
||||||
const arch = cabinets.filter((c) => !c.isBasket).length;
|
const arch = cabinets.filter((c) => !c.isBasket).length;
|
||||||
const bk = cabinets.filter((c) => c.isBasket).length;
|
const bk = cabinets.filter((c) => c.isBasket).length;
|
||||||
setStatus(`Verbindung ok – ${arch} Archive, ${bk} Briefkörbe gefunden.`, "ok");
|
const warn = insecureWarning(s.serverUrl);
|
||||||
|
setStatus(`Verbindung ok – ${arch} Archive, ${bk} Briefkörbe gefunden.` + warn, warn ? "err" : "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(`Fehler: ${e.message}`, "err");
|
setStatus(`Fehler: ${e.message}`, "err");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("exportBtn").addEventListener("click", async () => {
|
$("mapAdd").addEventListener("click", () => addMapRow());
|
||||||
const s = await Settings.get();
|
$("mapReset").addEventListener("click", () => loadMapping(DEFAULT_FIELD_MAP));
|
||||||
const blob = new Blob([JSON.stringify(s, null, 2)], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
// --- Mapping teilen: JSON anzeigen / kopieren / anwenden / von URL laden ----
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
$("mappingExport").addEventListener("click", () => {
|
||||||
a.download = "docuware-ablage-einstellungen.json";
|
$("mappingJson").value = JSON.stringify(collectMapping(), null, 2);
|
||||||
document.body.appendChild(a);
|
setStatus("Aktuelles Mapping als JSON angezeigt.", "ok");
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
||||||
setStatus("Einstellungen exportiert.", "ok");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("importBtn").addEventListener("click", () => $("importFile").click());
|
$("mappingCopy").addEventListener("click", async () => {
|
||||||
|
let txt = $("mappingJson").value.trim();
|
||||||
$("importFile").addEventListener("change", async (e) => {
|
if (!txt) { txt = JSON.stringify(collectMapping(), null, 2); $("mappingJson").value = txt; }
|
||||||
const file = e.target.files && e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
try {
|
try {
|
||||||
const text = await file.text();
|
await navigator.clipboard.writeText(txt);
|
||||||
const data = JSON.parse(text);
|
setStatus("In Zwischenablage kopiert.", "ok");
|
||||||
await Settings.set(data);
|
} catch (_) {
|
||||||
await load();
|
$("mappingJson").select(); document.execCommand("copy");
|
||||||
setStatus("Einstellungen importiert.", "ok");
|
setStatus("In Zwischenablage kopiert.", "ok");
|
||||||
} catch (err) {
|
|
||||||
setStatus(`Import fehlgeschlagen: ${err.message}`, "err");
|
|
||||||
} finally {
|
|
||||||
e.target.value = "";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$("diagBtn").addEventListener("click", async () => {
|
$("mappingApply").addEventListener("click", () => {
|
||||||
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 {
|
try {
|
||||||
const s = await Settings.set(collect());
|
const obj = JSON.parse($("mappingJson").value);
|
||||||
// Sitzungs-Token nutzen; falls noch keiner da ist, Passwortfeld verwenden.
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("kein JSON-Objekt");
|
||||||
const st = await browser.runtime.sendMessage({ type: "auth:status" });
|
loadMapping(obj);
|
||||||
if (st && st.hasToken && st.token) {
|
setStatus("JSON ins Mapping übernommen – mit „Speichern“ sichern.", "ok");
|
||||||
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);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
out.value = `Fehler: ${e.message}`;
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Archiv wählen -> dessen Indexfelder als Vorschläge (datalist) für alle Feldnamen.
|
||||||
|
$("mapCabinet").addEventListener("change", async () => {
|
||||||
|
const id = $("mapCabinet").value;
|
||||||
|
const st = $("mapFieldStatus");
|
||||||
|
if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; }
|
||||||
|
st.textContent = "Lade Felder …";
|
||||||
|
try {
|
||||||
|
const s = await Settings.get();
|
||||||
|
const ok = await ensureToken();
|
||||||
|
if (!ok) { st.textContent = "Erst „Verbindung testen“ (Passwort nötig)."; return; }
|
||||||
|
availableFields = (await DocuWare.cabinetFieldNames(s, id))
|
||||||
|
.filter((n) => n && !/^DW[A-Z]/.test(n)) // DW*-Systemfelder ausblenden
|
||||||
|
.sort();
|
||||||
|
refreshFieldSelects();
|
||||||
|
st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
|
||||||
|
} catch (e) {
|
||||||
|
st.textContent = `Fehler: ${e.message}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
24
updates.json
24
updates.json
@ -13,6 +13,30 @@
|
|||||||
{
|
{
|
||||||
"version": "0.10.2",
|
"version": "0.10.2",
|
||||||
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.2/docuware-ablage-0.10.2.xpi"
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.2/docuware-ablage-0.10.2.xpi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.10.3",
|
||||||
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.3/docuware-ablage-0.10.3.xpi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.10.4",
|
||||||
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.4/docuware-ablage-0.10.4.xpi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.10.5",
|
||||||
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.5/docuware-ablage-0.10.5.xpi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.10.6",
|
||||||
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.6/docuware-ablage-0.10.6.xpi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.10.7",
|
||||||
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.7/docuware-ablage-0.10.7.xpi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.10.8",
|
||||||
|
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.8/docuware-ablage-0.10.8.xpi"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user