360 lines
13 KiB
JavaScript
360 lines
13 KiB
JavaScript
|
|
// 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<Array<{id, name, color, isBasket}>>}
|
||
|
|
*/
|
||
|
|
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<Array<{id, name, type:"store"|"search"}>>}
|
||
|
|
*/
|
||
|
|
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<Array<{name, label, type, required, isSelectList}>>}
|
||
|
|
*/
|
||
|
|
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<string,{value:any,type:string}>} 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 };
|