// DocuWare Platform REST-Client. // Alle Pfade relativ zu {serverUrl}/DocuWare/Platform. // Auth läuft per Session-Cookie (siehe auth.js); daher überall credentials:"include". const DocuWare = { async _get(settings, path) { const platform = Settings.platformUrl(settings); const res = await fetch(`${platform}${path}`, { method: "GET", credentials: "include", headers: { Accept: "application/json", ...Auth.authHeaders() }, }); if (!res.ok) { const t = await res.text().catch(() => ""); throw new Error(`GET ${path} -> HTTP ${res.status}. ${t}`.trim()); } return res.json(); }, /** * Liste der Ablageziele: Archive UND Briefkörbe. * @returns {Promise>} */ async listCabinets(settings) { const data = await this._get(settings, "/FileCabinets"); const items = data.FileCabinet || []; return items.map((c) => ({ id: c.Id, name: c.Name, color: c.Color, isBasket: !!c.IsBasket, })); }, /** Dialoge eines Schranks. Liefert den ersten Store-Dialog (oder null). */ async getStoreDialogId(settings, cabinetId) { const dialogs = await this._dialogList(settings, cabinetId); const store = dialogs.find((d) => this._isStore(d)); return store ? store.Id : null; }, /** * Wählt den Dialog, aus dem die Indexfelder gelesen werden: * bevorzugt der Store-Dialog, sonst ein Suchdialog (mit echter GUID). * Gespeichert wird per API auch ohne Store-Dialog (Fields-only Upload). * @returns {Promise<{id:string, type:"store"|"search"}|null>} */ async getFieldsDialog(settings, cabinetId) { const dialogs = await this._dialogList(settings, cabinetId); const store = dialogs.find((d) => this._isStore(d)); if (store) return { id: store.Id, type: "store" }; const searches = dialogs.filter( (d) => (d.Type || d.DialogType || "").toLowerCase() === "search" ); const real = searches.find( (d) => d.Id && d.Id !== "00000000-0000-0000-0000-000000000000" ); const pick = real || searches[0]; return pick ? { id: pick.Id, type: "search" } : null; }, /** * Alle Dialoge, aus denen Indexfelder gelesen werden können: * bevorzugt ALLE Store-Dialoge; gibt es keinen, die Suchdialoge. * @returns {Promise>} */ async listFieldsDialogs(settings, cabinetId) { const dialogs = await this._dialogList(settings, cabinetId); const stores = dialogs .filter((d) => this._isStore(d)) .map((d) => ({ id: d.Id, name: d.DisplayName || d.Name || "Ablagedialog", type: "store" })); if (stores.length) return stores; const searches = dialogs .filter((d) => (d.Type || d.DialogType || "").toLowerCase() === "search") .map((d) => ({ id: d.Id, name: d.DisplayName || d.Name || "Suchdialog", type: "search" })); const real = searches.filter((d) => d.id && d.id !== "00000000-0000-0000-0000-000000000000"); return real.length ? real : searches; }, async _dialogList(settings, cabinetId) { const data = await this._get(settings, `/FileCabinets/${cabinetId}/Dialogs`); return this._asArray(data.Dialog).length ? this._asArray(data.Dialog) : this._asArray(data.Dialogs); }, _isStore(d) { return (d.Type || d.DialogType || "").toLowerCase() === "store" || d.IsForStoring; }, /** * Felddefinitionen eines Store-Dialogs. * Robust gegen die verschiedenen DocuWare-JSON-Formen: * Fields (flaches Array) | Fields.Field | Field | Items. * Feldtypen werden zusätzlich aus den Schrank-Stammdaten ergänzt, falls der * Dialog sie nicht mitliefert (wichtig für korrekte Datums-/Zahlenformate). * @returns {Promise>} */ async getStoreFields(settings, cabinetId, dialogId) { const data = await this._get( settings, `/FileCabinets/${cabinetId}/Dialogs/${dialogId}` ); let raw = this._fieldArray(data); if (!raw.length) { // Diagnose: zeigt die tatsächliche Struktur, damit wir das Format sehen. throw new Error( `Dialog lieferte keine Felder. Top-Level-Keys: [${Object.keys(data).join(", ")}].` ); } return raw .map((f) => { const name = f.DBFieldName || f.DBName || f.FieldName || f.Name; const hasList = f.SelectListsAssigned === true || f.AssignedInternalSelectList === true || (Array.isArray(f.Links) && f.Links.some((l) => l.rel === "simpleSelectList")); return { name, label: f.DlgLabel || f.FieldLabel || f.Label || f.DisplayName || name, type: f.DWFieldType || f.FieldType || f.Type || "Text", required: f.NotEmpty === true || f.Required === true || f.Mandatory === true, readOnly: f.ReadOnly === true || f.Locked === true, visible: f.Visible !== false, isSelectList: hasList, selectListOnly: f.SelectListOnly === true, prefill: this._prefillValue(f), }; }) .filter( (f) => f.name && f.visible && !this._isSystemField(f.name) && !/table/i.test(f.type) // Tabellenfelder unterstützt v1 nicht ); }, /** Liest den vordefinierten Default-Wert (PrefillValue) eines Feldes. */ _prefillValue(f) { const pv = f.PrefillValue || f.DefaultItem || f.Default; if (pv === undefined || pv === null) return ""; if (Array.isArray(pv)) { const items = pv .map((p) => (p && typeof p === "object" ? p.Item : p)) .filter((x) => x !== null && x !== undefined && x !== ""); return items.join("; "); } if (typeof pv === "object") return pv.Item != null ? String(pv.Item) : ""; return String(pv); }, /** Map DBFieldName -> {type, label, isSelectList} aus /FileCabinets/{id}. */ async _cabinetFieldMap(settings, cabinetId) { const data = await this._get(settings, `/FileCabinets/${cabinetId}`); const raw = this._fieldArray(data); const map = {}; raw.forEach((f) => { const name = f.DBFieldName || f.DBName || f.Name; if (!name) return; map[name] = { type: f.DWFieldType || f.FieldType || f.Type || "Text", label: f.FieldLabel || f.DlgLabel || f.Label || f.DisplayName || name, isSelectList: !!f.Keywords || !!f.HasSelectList || !!f.SelectList, }; }); return map; }, /** Holt die Feldliste aus den gängigen Container-Formen. */ _fieldArray(data) { if (!data) return []; if (this._asArray(data.Fields).length) return this._asArray(data.Fields); if (data.Fields && this._asArray(data.Fields.Field).length) return this._asArray(data.Fields.Field); if (this._asArray(data.Field).length) return this._asArray(data.Field); if (this._asArray(data.Items).length) return this._asArray(data.Items); return []; }, _asArray(x) { return Array.isArray(x) ? x : []; }, /** * DIAGNOSE: liefert die echten Rohstrukturen, damit Auswahllisten- und * Default-Parsing exakt (statt geraten) implementiert werden kann. */ async diagnoseStoreDialog(settings, cabinetId) { const out = {}; const dialogs = await this._dialogList(settings, cabinetId); out.dialogs = dialogs.map((d) => ({ Id: d.Id, Type: d.Type || d.DialogType, Name: d.DisplayName || d.Name, })); const pick = await this.getFieldsDialog(settings, cabinetId); out.usedDialog = pick; // welcher Dialog für die Felder genutzt wird if (!pick) { out.note = "Weder Store- noch Suchdialog gefunden."; return out; } const detail = await this._get( settings, `/FileCabinets/${cabinetId}/Dialogs/${pick.id}` ); const arr = this._fieldArray(detail); out.detailTopLevelKeys = Object.keys(detail); out.fieldCount = arr.length; out.sampleFields = arr.slice(0, 10); // genug, um das Schema zu erkennen // Roh-Antwort einer Auswahlliste am ersten Feld, um deren Form zu sehen. if (arr.length) { const fname = arr[0].DBFieldName || arr[0].DBName || arr[0].FieldName || arr[0].Name; out.sampleSelectListField = fname; try { const platform = Settings.platformUrl(settings); const url = `${platform}/FileCabinets/${cabinetId}/Query/SelectListExpression` + `?DialogId=${encodeURIComponent(pick.id)}&FieldName=${encodeURIComponent(fname)}`; const res = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json", ...Auth.authHeaders(), }, body: JSON.stringify({ ValuePrefix: "", Limit: 25, Typed: false, ExcludeExternal: true }), }); out.sampleSelectListStatus = res.status; out.sampleSelectListRaw = await res.json().catch(() => "(kein JSON)"); } catch (e) { out.sampleSelectListError = String(e); } } return out; }, /** * Werte einer Auswahlliste für ein Feld. * POST /FileCabinets/{id}/Query/SelectListExpression?DialogId=..&FieldName=.. */ async getSelectList(settings, cabinetId, dialogId, fieldName, prefix = "") { const platform = Settings.platformUrl(settings); const url = `${platform}/FileCabinets/${cabinetId}/Query/SelectListExpression` + `?DialogId=${encodeURIComponent(dialogId)}&FieldName=${encodeURIComponent(fieldName)}`; const res = await fetch(url, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json", ...Auth.authHeaders(), }, body: JSON.stringify({ ValuePrefix: prefix, Limit: 1000, Typed: false, ExcludeExternal: false, // auch fest hinterlegte Listen liefern }), }); if (!res.ok) return []; const data = await res.json().catch(() => ({})); // Antwort-Form variiert: Value/Values/List(.Value) mit Strings oder Objekten. const raw = data.Value || data.Values || (data.List && (data.List.Value || data.List.Values)) || []; return raw .map((v) => (v && typeof v === "object" ? v.Item ?? v.Value ?? v.Name ?? "" : v)) .filter((v) => v !== "" && v !== null && v !== undefined) .map(String); }, /** * Lädt eine Datei mit Indexfeldern in einen Schrank hoch. * @param {File|Blob} file Inhalt (eml/pdf/Anhang) * @param {string} fileName Dateiname inkl. Endung * @param {Object} fields Indexfelder * @returns {Promise<{id:number}>} */ async uploadDocument(settings, cabinetId, file, fileName, fields) { const platform = Settings.platformUrl(settings); const indexJson = JSON.stringify({ Fields: this._buildFields(fields) }); const form = new FormData(); // Reihenfolge wichtig: erst Index-Teil ("Document"), dann Datei ("File[]"). // Part-Namen exakt wie in der DocuWare-API-Collection. form.append( "Document", new Blob([indexJson], { type: "application/json" }), "Index.json" ); form.append("File[]", file, fileName); const res = await fetch(`${platform}/FileCabinets/${cabinetId}/Documents`, { method: "POST", credentials: "include", headers: { Accept: "application/json", ...Auth.authHeaders() }, body: form, }); if (!res.ok) { const t = await res.text().catch(() => ""); throw new Error(`Upload fehlgeschlagen (HTTP ${res.status}). ${t}`.trim()); } const doc = await res.json().catch(() => ({})); return { id: doc.Id, raw: doc }; }, // --- Helfer --------------------------------------------------------------- /** Wandelt {name:{value,type}} in das DocuWare-Field-Array. */ _buildFields(fields) { const out = []; for (const [name, def] of Object.entries(fields)) { const value = def && typeof def === "object" ? def.value : def; const type = def && typeof def === "object" ? def.type : "Text"; if (value === undefined || value === null || value === "") continue; out.push(this._field(name, value, type)); } return out; }, _field(name, value, type) { const t = (type || "Text").toLowerCase(); if (t.includes("date")) { // DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform. const d = value instanceof Date ? value : new Date(value); return { FieldName: name, Item: d.toISOString(), ItemElementName: "Date" }; } if (t.includes("decimal") || t.includes("currency")) { return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" }; } if (t.includes("numeric") || t.includes("int")) { return { FieldName: name, Item: Number(value), ItemElementName: "Int" }; } if (t.includes("memo")) { return { FieldName: name, Item: String(value), ItemElementName: "Memo" }; } if (t.includes("keyword")) { const arr = Array.isArray(value) ? value : [String(value)]; return { FieldName: name, Item: arr, ItemElementName: "Keywords" }; } return { FieldName: name, Item: String(value), ItemElementName: "String" }; }, _isSystemField(dbName) { if (!dbName) return true; // DW-interne Systemfelder (DWxxx) und Volltext nicht im Dialog anzeigen. return /^DW[A-Z]/.test(dbName) || dbName === "DocuWareFulltext"; }, }; if (typeof module !== "undefined") module.exports = { DocuWare };