PowerBI Apps und Berichte mit Synapcus-Daten
1. Ziel und Architekturüberblick
Die Synapcus REST API wird eingesetzt, um strukturierte Daten automatisiert in Power BI zu laden und dort auszuwerten.
Synapcus stellt hierfür mehrere REST-Endpunkte bereit, über die fachliche Objekte im JSON-Format abgefragt werden können, z. B.:
Projekte (prj, pos, perprj)
Vertrieb (lea, sop, sac)
Zeiten (tim)
Personal (stf, emp, yes, mos)
vorbereitete Reports (rep)
Die REST API dient als standardisierte Datenschnittstelle.
Die fachliche Verarbeitung erfolgt abhängig vom gewählten Integrationsansatz entweder:
zentral in Synapcus (Report-Variante)
oder im Zielsystem (Live-Daten-Variante)
2. Technische Voraussetzungen
2.1 Power BI Desktop
Für die Entwicklung und Einrichtung der Schnittstelle ist Power BI Desktop erforderlich.
Power BI Desktop wird verwendet für:
Einrichtung der REST-Verbindung
Implementierung und Test von API-Abfragen (Power Query / M-Code)
Transformation der JSON-Daten
Modellierung
Veröffentlichung in den Power BI Service
Download:
https://www.microsoft.com/en-us/download/details.aspx?id=58494
2.2 Erreichbarkeit der REST-API
Die vollständige REST-URL muss erreichbar sein.
Beispiel:
https://[kundeninstanz].synapcus.com/[kundeninstanz]/synapcus.nsf/xrReps.xsp/rest/api
Nicht nur /rest/api, sondern der vollständige Endpunkt inklusive Query-Parameter muss eine gültige JSON-Antwort liefern.
Erforderlich:
DNS-Auflösung
HTTPS-Zugriff
gültiges TLS-Zertifikat
Firewall-Freigabe
kein Proxy-Blocking
2.3 Authentifizierung
Der Zugriff erfolgt über die in Synapcus konfigurierten Sicherheitsmechanismen.
Typischerweise:
HTTP Basic Authentication
optional API-Token
Bei Basic Authentication wird ein technischer Benutzer benötigt.
Header-Beispiel:
Authorization: Basic <Base64(username:password)>
Anforderungen an den technischen Benutzer:
Leserechte auf die entsprechende Ressource
Zugriff auf
xrReps.xspoder sonstig Endpunktekeine interaktive Login-Weiterleitung
Die Zugangsdaten dürfen nicht im M-Code gespeichert werden.
Sie werden in Power BI sicher im Credential Store hinterlegt.
2.4 Tabellen, Entitäten und Felder
Zur Erstellung von PowerBI-Reports auf Basis von Synapcus-Daten ist ein Verständnis der Synapcus-Notationen, Entitäten, Tabellen und Felder erforderlich. Die korrekte Interpretation der Datenstruktur ist Voraussetzung für eine fachlich und technisch saubere Modellierung in PowerBI.
Weiterführende Details zur Datenstruktur und den technischen Grundlagen sind im Developers Guide dokumentiert.
https://synapcus-developers.scrollhelp.site/syndev/entities-and-fields
3. Integrationsvarianten
Grundsätzlich existieren zwei Möglichkeiten, Daten aus Synapcus in Power BI zu laden.
Variante 1 – Nutzung vorbereiteter Synapcus-Reports
3.1 Grundprinzip
Bei dieser Methode werden in Synapcus generierte Reports über die REST API abgerufen.
Synapcus übernimmt:
Aggregation
KPI-Berechnung
fachliche Logik
Snapshot-Erzeugung
Power BI übernimmt ausschließlich:
Abruf
Parsing
Visualisierung
Es erfolgt keine Neuberechnung der Kennzahlen im Zielsystem.
3.2 Periodische Report-Erzeugung
Reports werden regelmäßig erzeugt und bilden definierte Datenstände. Bei Bedarf lassen sich Reports auch manuell generieren und speichern.
Beispiele:
Projekt-Controlling → wöchentlich
Personaldaten → monatlich
Die API (xrReps/xrRep) liefert ausschließlich bereits generierte Reports.
Die Datenaktualität entspricht rep_creation_date.
3.3 Beispiel: HRM Personal-Report (hrmcmpdash)
Voraussetzung:
Ein aktueller Report vom Typ:
"Personal"
mit internem Typ:
"hrmcmpdash"
Bei der manuellen Generierung wählt man den Bereichstyp „Personaldaten“ aus.

3.4 REST-Call
Beispiel:
https://[kundeninstanz].synapcus.com/[kundeninstanz]/synapcus.nsf/xrReps.xsp/rest/api?query=[rep_type]=hrmcmpdash&responce=dttb
Parameterbeschreibung
query
query=[rep_type]=hrmcmpdash
Filtert nach Report-Typ.
responce
responce=dttb
Definiert JSON-DataTable-Struktur.
Hinweis: „responce“ ist API-seitig so definiert.
3.5 Struktur der API-Antwort
{
"data": [
{
"rep_nr": "...",
"rep_name": "...",
"rep_body": "-- HIER BEFINDEN SICH DIE JSON-DATEN --",
"rep_creation_date": "..."
}
]
}
rep_body enthält verschachteltes JSON.
Beispiel-Arrays:
stfsByOU1Cnt
stfsByProfGrpCnt
Typische Felder:
Feld | Bedeutung |
|---|---|
vl | numerischer Wert |
lbl | Bezeichnung |
3.6 Vollständiges Implementierungsbeispiel (M-Code)
Der folgende Code zeigt die strukturierte Verarbeitung des HRM-Reports.
Synapcus-Daten holen und transformieren für die Tabelle “stfs”:
// Synapcus Daten transformieren für die Tabelle stfs
let
#"Dash Daten" =
let
// Load JSON data from the web API
Quelle =
Json.Document(
Web.Contents(
"https://synapcus.kohlbecker.de/kohlbecker/synapcus.nsf/xrReps.xsp/rest/api?query=[rep_type]=hrmcmpdash&responce=dttb"
)
),
DebugQuelle = Quelle, // Debug: Log raw response
data = Quelle[data],
DebugData = data, // Debug: Log raw data from the response
// Filter the data to include only the required fields
filteredData =
List.Select(
data,
each Record.HasFields(_, {"rep_nr", "rep_name", "rep_body", "rep_creation_date"})
),
DebugFilteredData = Table.FromRecords(filteredData), // Debug: Log filtered rows
// ---- ONLY USE LAST RECORD (newest by rep_creation_date) ----
lastRow =
List.Last(
List.Sort(
filteredData,
(a, b) =>
Value.Compare(
try DateTime.From(a[rep_creation_date]) otherwise #datetime(1900, 1, 1, 0, 0, 0),
try DateTime.From(b[rep_creation_date]) otherwise #datetime(1900, 1, 1, 0, 0, 0)
)
)
),
// Transform each row: parse rep_body and extract necessary arrays
TransformData = (row) =>
let
DebugRow = row, // Debug: Log the current row
// Parse rep_body as JSON
repBody = try Json.Document(row[rep_body]) otherwise null,
DebugRepBody = repBody, // Debug: Log the parsed rep_body
// Extract "stfsByOU1Cnt" and "stfsByProfGrpCnt"
stfsByOU1Cnt =
try
if repBody <> null and Record.HasFields(repBody{0}, "stfsByOU1Cnt") then
repBody{0}[stfsByOU1Cnt]
else
null
otherwise
null,
DebugStfsByOU1Cnt = stfsByOU1Cnt, // Debug: Log "stfsByOU1Cnt"
stfsByProfGrpCnt =
try
if repBody <> null and Record.HasFields(repBody{0}, "stfsByProfGrpCnt") then
repBody{0}[stfsByProfGrpCnt]
else
null
otherwise
null,
DebugStfsByProfGrpCnt = stfsByProfGrpCnt, // Debug: Log "stfsByProfGrpCnt"
// Convert "stfsByOU1Cnt" into a table
ouTable =
if stfsByOU1Cnt <> null then
let
table = Table.FromList(stfsByOU1Cnt, Splitter.SplitByNothing(), {"OU_Record"}),
DebugTableOU = table, // Debug: Log the intermediate OU table
expanded = Table.ExpandRecordColumn(table, "OU_Record", {"vl", "lbl"}),
DebugExpandedOU = expanded, // Debug: Log the expanded OU table
withParentFields = Table.AddColumn(expanded, "rep_creation_date", each row[rep_creation_date])
in
withParentFields
else
null,
// Convert "stfsByProfGrpCnt" into a table
profTable =
if stfsByProfGrpCnt <> null then
let
table = Table.FromList(stfsByProfGrpCnt, Splitter.SplitByNothing(), {"ProfGrp_Record"}),
DebugTableProfGrp = table, // Debug: Log the intermediate ProfGrp table
expanded = Table.ExpandRecordColumn(table, "ProfGrp_Record", {"vl", "lbl"}),
DebugExpandedProfGrp = expanded, // Debug: Log the expanded ProfGrp table
withParentFields = Table.AddColumn(expanded, "rep_creation_date", each row[rep_creation_date])
in
withParentFields
else
null
in
[OU_Table = ouTable, ProfGrp_Table = profTable],
// Apply transformation to ONLY the last row
TransformDataList = { TransformData(lastRow) },
DebugTransformDataList = TransformDataList, // Debug: Log transformed data list
// Combine all OU tables into a single table
ouCombinedTable = Table.Combine(List.RemoveNulls(List.Transform(TransformDataList, each _[OU_Table]))),
DebugCombinedOU = ouCombinedTable, // Debug: Log combined OU table
profCombinedTable = Table.Combine(List.RemoveNulls(List.Transform(TransformDataList, each _[ProfGrp_Table]))),
DebugCombinedProfGrp = profCombinedTable, // Debug: Log combined ProfGrp table
// Rename columns for clarity
renamedOuTable =
Table.RenameColumns(
ouCombinedTable,
{{"vl", "Value"}, {"lbl", "Label"}, {"rep_creation_date", "Creation Date"}}
),
DebugRenamedOU = renamedOuTable, // Debug: Log renamed OU table
renamedProfTable =
Table.RenameColumns(
profCombinedTable,
{{"vl", "Value"}, {"lbl", "Label"}, {"rep_creation_date", "Creation Date"}}
),
DebugRenamedProfGrp = renamedProfTable, // Debug: Log renamed ProfGrp table
// Add tables for Power BI KPI visuals
combinedForKPI =
Table.Combine(
{
Table.AddColumn(renamedOuTable, "Category", each "stfsByOU1Cnt"),
Table.AddColumn(renamedProfTable, "Category", each "stfsByProfGrpCnt")
}
),
DebugCombinedForKPI = combinedForKPI, // Debug: Log combined KPI table
// Final table for KPIs
FinalKPI = Table.SelectColumns(combinedForKPI, {"Category", "Label", "Value", "Creation Date"}),
DebugFinalKPI = FinalKPI // Debug: Log final KPI table
in
[
DebugQuelle = DebugQuelle,
DebugData = DebugData,
DebugFilteredData = DebugFilteredData,
DebugTransformDataList = DebugTransformDataList,
DebugCombinedOU = DebugCombinedOU,
DebugCombinedProfGrp = DebugCombinedProfGrp,
DebugFinalKPI = DebugFinalKPI
],
DebugFinalKPI = #"Dash Daten"[DebugFinalKPI],
#"Umbenannte Spalten" =
Table.RenameColumns(
DebugFinalKPI,
{{"Label", "Name"}, {"Value", "MA"}, {"Creation Date", "Erstelldatum"}}
)
in
#"Umbenannte Spalten"
Synapcus-Daten holen und transformieren für die Tabelle “emps”:
let
// JSON-Daten abrufen
Quelle = Json.Document(Web.Contents("https://synapcus.kohlbecker.de/kohlbecker/synapcus.nsf/xrReps.xsp/rest/api?query=[rep_type]=hrmcmpdash&responce=dttb")),
DebugQuelle = Quelle,
data = Quelle[data],
DebugData = data,
// Daten filtern, um nur relevante Felder zu behalten
filteredData = List.Select(data, each Record.HasFields(_, {"rep_nr", "rep_name", "rep_body", "rep_creation_date"})),
DebugFilteredData = Table.FromRecords(filteredData),
// ---- ONLY USE LAST RECORD (newest by rep_creation_date) ----
lastRow =
List.Last(
List.Sort(
filteredData,
(a, b) =>
Value.Compare(
try DateTime.From(a[rep_creation_date]) otherwise #datetime(1900, 1, 1, 0, 0, 0),
try DateTime.From(b[rep_creation_date]) otherwise #datetime(1900, 1, 1, 0, 0, 0)
)
)
),
// Transformation: rep_body in Tabellen umwandeln
TransformData = (row) =>
let
DebugRow = row,
repBody = try Json.Document(row[rep_body]) otherwise null,
DebugRepBody = repBody,
stfsByOU1Cnt = try if repBody <> null and Record.HasFields(repBody{0}, "stfsByOU1Cnt") then repBody{0}[stfsByOU1Cnt] else null otherwise null,
stfsByProfGrpCnt = try if repBody <> null and Record.HasFields(repBody{0}, "stfsByProfGrpCnt") then repBody{0}[stfsByProfGrpCnt] else null otherwise null,
ouTable = if stfsByOU1Cnt <> null then
let
table = Table.FromList(stfsByOU1Cnt, Splitter.SplitByNothing(), {"OU_Record"}),
expanded = Table.ExpandRecordColumn(table, "OU_Record", {"vl", "lbl"}),
withParentFields = Table.AddColumn(expanded, "rep_creation_date", each row[rep_creation_date])
in
withParentFields
else
null,
profTable = if stfsByProfGrpCnt <> null then
let
table = Table.FromList(stfsByProfGrpCnt, Splitter.SplitByNothing(), {"ProfGrp_Record"}),
expanded = Table.ExpandRecordColumn(table, "ProfGrp_Record", {"vl", "lbl"}),
withParentFields = Table.AddColumn(expanded, "rep_creation_date", each row[rep_creation_date])
in
withParentFields
else
null,
emps = try if repBody <> null and Record.HasFields(repBody{0}, "emps") then repBody{0}[emps] else null otherwise null,
empTable = if emps <> null then
let
table = Table.FromList(emps, Splitter.SplitByNothing(), {"Emp_Record"}),
expanded = Table.ExpandRecordColumn(table, "Emp_Record", {
"emp_per_name",
"emp_ou1_name",
"emp_ou2_name",
"emp_ou3_name",
"emp_country_state",
"emp_hourly_fee",
"emp_org_function",
"emp_org_function_text",
"emp_ser_name",
"emp_type",
"emp_wp_days_type_text",
"emp_soll_work_year_d",
"emp_soll_lev_year_d",
"yes_total_hrs",
"yes_soll_lev_sum_yesd",
"yes_state",
"yes_soll_lev_extra_yesd",
"yes_payed_total_hrs",
"yes_soll_lev_rest_d"
}),
withParentFields = Table.AddColumn(expanded, "rep_creation_date", each row[rep_creation_date])
in
withParentFields
else
null
in
[
OU_Table = ouTable,
ProfGrp_Table = profTable,
Emp_Table = empTable
],
TransformDataList = { TransformData(lastRow) },
ouCombinedTable = Table.Combine(List.RemoveNulls(List.Transform(TransformDataList, each _[OU_Table]))),
profCombinedTable = Table.Combine(List.RemoveNulls(List.Transform(TransformDataList, each _[ProfGrp_Table]))),
empCombinedTable = Table.Combine(List.RemoveNulls(List.Transform(TransformDataList, each _[Emp_Table]))),
renamedOuTable = Table.RenameColumns(ouCombinedTable, {{"vl", "Value"}, {"lbl", "Label"}, {"rep_creation_date", "Creation Date"}}),
renamedProfTable = Table.RenameColumns(profCombinedTable, {{"vl", "Value"}, {"lbl", "Label"}, {"rep_creation_date", "Creation Date"}}),
combinedForKPI = Table.Combine({
Table.AddColumn(renamedOuTable, "Category", each "stfsByOU1Cnt"),
Table.AddColumn(renamedProfTable, "Category", each "stfsByProfGrpCnt")
}),
FinalKPI = Table.SelectColumns(combinedForKPI, {"Category", "Label", "Value", "Creation Date"}),
Emp_FinalTable = if empCombinedTable <> null then
let
empRenamed = Table.RenameColumns(empCombinedTable, {
{"emp_per_name", "Employee Name"},
{"emp_hourly_fee", "Hourly Fee"},
{"emp_org_function", "Function"},
{"emp_type", "Employee Type"},
{"emp_wp_days_type_text", "Work Pattern"},
{"emp_soll_work_year_d", "Total Work Days"},
{"emp_soll_lev_year_d", "Leave Days"},
{"yes_total_hrs", "Total Hours"},
{"yes_soll_lev_sum_yesd", "Leave Sum"},
{"yes_state", "Employee State"},
{"yes_soll_lev_extra_yesd", "Extra Leave"},
{"yes_payed_total_hrs", "Paid Hours"},
{"yes_soll_lev_rest_d", "Remaining Leave"},
{"rep_creation_date", "Creation Date"}
})
in
empRenamed
else
#table({"Employee Name", "Hourly Fee", "Function", "Employee Type", "Work Pattern", "Total Work Days", "Leave Days", "Total Hours", "Leave Sum", "Employee State", "Extra Leave", "Paid Hours", "Remaining Leave", "Creation Date"}, {}),
DebugFinalKPI = FinalKPI,
DebugEmpTable = Emp_FinalTable,
// Datentypen korrigieren (nur Zahlen mit 2 Dezimalstellen, ohne Währungsformat)
Emp_Typed = Table.TransformColumnTypes(Emp_FinalTable, {
{"Hourly Fee", type number}, // Will be formatted in Power BI as Currency
{"Total Work Days", type number},
{"Leave Days", type number},
{"Total Hours", type number},
{"Leave Sum", type number},
{"Extra Leave", type number},
{"Paid Hours", type number},
{"Remaining Leave", type number},
{"Creation Date", type text}
}),
// Währungsformat explizit für "Hourly Fee" setzen
Emp_CurrencyFormatted = Table.TransformColumns(Emp_Typed, {{"Hourly Fee", each Number.Round(_, 2), type number}}),
#"Geänderter Typ" = Table.TransformColumnTypes(Emp_CurrencyFormatted,{{"Leave Days", type number}}),
#"Umbenannte Spalten" = Table.RenameColumns(#"Geänderter Typ",{{"emp_ser_name", "Leistung"}, {"Hourly Fee", "Stundensatz"}, {"Leave Days", "Urlaub-Soll"}})
in
#"Umbenannte Spalten"
3.7 Bewertung Variante 1
Vorteile:
zentrale KPI-Definition
geringe Komplexität
hohe Konsistenz
klare Snapshot-Struktur
Einschränkungen:
geringe Flexibilität
keine Echtzeitberechnung
Abhängigkeit von Report-Struktur
Variante 2 – Zugriff auf Live-Daten
4.1 Grundprinzip
Hier werden operative Synapcus-Daten direkt über REST-Endpunkte abgefragt.
Es existieren:
keine voraggregierten Kennzahlen
keine Snapshot-Struktur
keine vorberechneten KPIs
Alle Berechnungen erfolgen im Zielsystem.
4.2 Charakteristik
Zugriff auf Detaildaten
flexible Filterung
hohe Individualisierung
größere Datenmengen
4.3 Anforderungen
Kenntnisse der Synapcus-Datenstruktur
sauberes Datenmodell
DAX-Measure-Definition
Performance-Optimierung
Die Zwischenspeicherung erfolgt im Power BI Service.
4.4 Architektur-Vergleich
Kriterium | Variante 1 | Variante 2 |
|---|---|---|
KPI-Logik | Synapcus | Power BI |
Flexibilität | Mittel | Hoch |
Komplexität | Gering | Hoch |
Wartung | Zentral | Verteilt |
Snapshot | Ja | Nein |
5. Betrieb und Monitoring
Für produktive Nutzung sind folgende Punkte sicherzustellen:
Überwachung von API-Erreichbarkeit
Monitoring von HTTP-Statuscodes
Handling von 401 / 403 / 500
Logging von Refresh-Fehlern
Versionierung bei JSON-Strukturänderungen
6. Fazit
Die Synapcus REST API ermöglicht eine strukturierte, sichere und skalierbare Integration in Power BI.
Variante 1 eignet sich für standardisierte, stabile KPI-Reports.
Variante 2 bietet maximale Flexibilität bei höherem Implementierungsaufwand.
Die Wahl der Architektur sollte auf Basis von:
Fachanforderungen
Aktualitätsbedarf
Wartbarkeit
Governance
getroffen werden.