Skip to main content
Skip table of contents

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:

CODE
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:

CODE
Authorization: Basic <Base64(username:password)>

Anforderungen an den technischen Benutzer:

  • Leserechte auf die entsprechende Ressource

  • Zugriff auf xrReps.xsp oder sonstig Endpunkte

  • keine 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:

CODE
"Personal"

mit internem Typ:

CODE
"hrmcmpdash"

Bei der manuellen Generierung wählt man den Bereichstyp „Personaldaten“ aus.

image-20260218-123525.png

3.4 REST-Call

Beispiel:

CODE
https://[kundeninstanz].synapcus.com/[kundeninstanz]/synapcus.nsf/xrReps.xsp/rest/api?query=[rep_type]=hrmcmpdash&responce=dttb

Parameterbeschreibung

query

CODE
query=[rep_type]=hrmcmpdash

Filtert nach Report-Typ.

responce

CODE
responce=dttb

Definiert JSON-DataTable-Struktur.

Hinweis: „responce“ ist API-seitig so definiert.

3.5 Struktur der API-Antwort

CODE
{
  "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.

  1. Synapcus-Daten holen und transformieren für die Tabelle “stfs”:

JS
// 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"

  1. Synapcus-Daten holen und transformieren für die Tabelle “emps”:

JS
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.

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.