ソースを参照

Vergleich mit meheren Vorgängen und Zeiträumen

Leonard Schulz 1 年間 前
親
コミット
e80676ebef

+ 98 - 0
src/main/java/com/tsystems/inwo/vgl/service/DataComparisonService.java

@@ -0,0 +1,98 @@
+package com.tsystems.inwo.vgl.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.tsystems.inwo.vgl.web.VergleichController.Pair;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+
+@Service
+public class DataComparisonService {
+
+    public <T> Map<String, Pair<JsonNode, JsonNode>> unionByKey(ArrayNode rightArr, ArrayNode leftArr, Function<JsonNode, String> keyFn) {
+        Map<String, Pair<JsonNode, JsonNode>> result = new LinkedHashMap<>();
+        
+        // Add all right items
+        for (JsonNode node : rightArr) {
+            String key = keyFn.apply(node);
+            result.put(key, new Pair<>(node, null));
+        }
+        
+        // Merge left items
+        for (JsonNode node : leftArr) {
+            String key = keyFn.apply(node);
+            result.merge(key, new Pair<>(null, node), (existing, newPair) -> 
+                new Pair<>(existing.right, newPair.left));
+        }
+        
+        return result;
+    }
+
+    public boolean detectPersonChanges(JsonNode rightItem, JsonNode rightPerson, JsonNode leftItem, JsonNode leftPerson) {
+        if (rightPerson == null || leftPerson == null) return false;
+        
+        // Check person fields
+        return !Objects.equals(rightPerson.path("nachname").asText(""), leftPerson.path("nachname").asText("")) ||
+               !Objects.equals(rightPerson.path("vorname").asText(""), leftPerson.path("vorname").asText("")) ||
+               !Objects.equals(rightPerson.path("geburtsdatum").asText(""), leftPerson.path("geburtsdatum").asText("")) ||
+               !Objects.equals(rightPerson.path("familienstand").asText(""), leftPerson.path("familienstand").asText("")) ||
+               !Objects.equals(rightPerson.path("geschlecht").asText(""), leftPerson.path("geschlecht").asText("")) ||
+               !Objects.equals(rightItem.path("verwandtschaftsverhaeltnis").asText(""), leftItem.path("verwandtschaftsverhaeltnis").asText("")) ||
+               !Objects.equals(rightPerson.path("sozialstellung").asText(""), leftPerson.path("sozialstellung").asText("")) ||
+               !Objects.equals(rightItem.path("rolleImHaushalt").asText(""), leftItem.path("rolleImHaushalt").asText("")) ||
+               rightPerson.path("zahlungsempfaenger").asBoolean(false) != leftPerson.path("zahlungsempfaenger").asBoolean(false);
+    }
+
+    public boolean detectEinkommenChanges(JsonNode rightItem, JsonNode leftItem) {
+        return !Objects.equals(rightItem.path("art").asText(""), leftItem.path("art").asText("")) ||
+               !Objects.equals(rightItem.path("artZusatz").asText(""), leftItem.path("artZusatz").asText("")) ||
+               rightItem.path("faktor").asDouble(12) != leftItem.path("faktor").asDouble(12) ||
+               rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0) ||
+               rightItem.path("zuschlag").asDouble(0.0) != leftItem.path("zuschlag").asDouble(0.0) ||
+               rightItem.path("abschlag").asDouble(0.0) != leftItem.path("abschlag").asDouble(0.0) ||
+               rightItem.path("werbungskosten").asDouble(0.0) != leftItem.path("werbungskosten").asDouble(0.0);
+    }
+
+    public boolean detectFreibetragChanges(JsonNode rightItem, JsonNode leftItem) {
+        return !Objects.equals(rightItem.path("art").asText(""), leftItem.path("art").asText("")) ||
+               rightItem.path("faktor").asDouble(12) != leftItem.path("faktor").asDouble(12) ||
+               rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0) ||
+               !Objects.equals(rightItem.path("fristDatum").asText(""), leftItem.path("fristDatum").asText(""));
+    }
+
+    public boolean detectKitaChanges(JsonNode rightItem, JsonNode leftItem) {
+        return !Objects.equals(rightItem.path("kindRef").asText(""), leftItem.path("kindRef").asText("")) ||
+               rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0);
+    }
+
+    public String getSymbolFor(String status) {
+        return switch (status) {
+            case "ADDED" -> "+";
+            case "REMOVED" -> "−";
+            case "CHANGED" -> "!";
+            default -> "";
+        };
+    }
+    
+    public String getSymbolClassFor(String status) {
+        return switch (status) {
+            case "ADDED" -> "plus";
+            case "REMOVED" -> "minus";
+            case "CHANGED" -> "warn";
+            default -> "";
+        };
+    }
+    
+    public String getRowClassFor(String status) {
+        return switch (status) {
+            case "ADDED" -> "diff-row-added";
+            case "REMOVED" -> "diff-row-removed";
+            default -> "";
+        };
+    }
+}

+ 111 - 0
src/main/java/com/tsystems/inwo/vgl/service/HtmlRenderingService.java

@@ -0,0 +1,111 @@
+package com.tsystems.inwo.vgl.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+@Service
+public class HtmlRenderingService {
+
+    public String buildPersonFieldHtml(JsonNode rightPerson, JsonNode leftPerson, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightPerson == null) {
+            JsonNode primary = rightPerson != null ? rightPerson : leftPerson;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightPerson != null ? extractor.apply(rightPerson) : "";
+        String leftVal = leftPerson != null ? extractor.apply(leftPerson) : "";
+        
+        if (!Objects.equals(rightVal, leftVal)) {
+            return cellHtml(rightVal, leftVal, true);
+        } else {
+            return escape(rightVal);
+        }
+    }
+    
+    public String buildItemFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightItem == null) {
+            JsonNode primary = rightItem != null ? rightItem : leftItem;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
+        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
+        
+        if (!Objects.equals(rightVal, leftVal)) {
+            return cellHtml(rightVal, leftVal, true);
+        } else {
+            return escape(rightVal);
+        }
+    }
+
+    public String buildEinkommenFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightItem == null) {
+            JsonNode primary = rightItem != null ? rightItem : leftItem;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
+        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
+        
+        if (!Objects.equals(rightVal, leftVal)) {
+            return cellHtml(rightVal, leftVal, true);
+        } else {
+            return escape(rightVal);
+        }
+    }
+
+    public String buildFreibetragFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightItem == null) {
+            JsonNode primary = rightItem != null ? rightItem : leftItem;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
+        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
+        
+        if (!Objects.equals(rightVal, leftVal)) {
+            return cellHtml(rightVal, leftVal, true);
+        } else {
+            return escape(rightVal);
+        }
+    }
+
+    public String buildKitaFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightItem == null) {
+            JsonNode primary = rightItem != null ? rightItem : leftItem;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
+        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
+        
+        if (!Objects.equals(rightVal, leftVal)) {
+            return cellHtml(rightVal, leftVal, true);
+        } else {
+            return escape(rightVal);
+        }
+    }
+
+    public String cellHtml(String newVal, String oldVal, boolean changed) {
+        if (!changed) {
+            return escape(newVal);
+        }
+        
+        // Handle empty old values
+        String displayOldVal = (oldVal == null || oldVal.trim().isEmpty()) ? "leer" : oldVal;
+        
+        // Return with cell-changed class and proper old value display
+        return "<span class='cell-changed'>" + escape(newVal) + 
+               "<div class='old'>Alt: " + escape(displayOldVal) + "</div></span>";
+    }
+    
+    public String escape(String s) {
+        if (s == null) return "";
+        return s.replace("&", "&")
+                .replace("<", "<")
+                .replace(">", ">");
+    }
+}

+ 274 - 0
src/main/java/com/tsystems/inwo/vgl/service/JsonNavigationService.java

@@ -0,0 +1,274 @@
+package com.tsystems.inwo.vgl.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.tsystems.inwo.vgl.web.VergleichController.Z;
+import com.tsystems.inwo.vgl.web.VergleichController.Vorgang;
+import com.tsystems.inwo.vgl.web.VergleichController.PersonRef;
+import com.tsystems.inwo.vgl.web.VergleichController.StammvorgangInfo;
+import com.tsystems.inwo.vgl.web.VergleichController.StammakteInfo;
+import com.tsystems.inwo.vgl.util.FormatUtil;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class JsonNavigationService {
+
+    public List<Z> extractZeitraeume(JsonNode root) {
+        List<Z> result = new ArrayList<>();
+        
+        // Check if this is the new stammakte structure
+        if (root.has("stammakte")) {
+            JsonNode stammakte = root.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
+                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                            String id = zeitraum.path("id").asText("");
+                            String beginn = zeitraum.path("beginn").asText("");
+                            String ende = zeitraum.path("ende").asText("");
+                            result.add(new Z(id, beginn, ende));
+                        }
+                    }
+                }
+            }
+        }
+        // Check if this is the vorgaenge structure
+        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
+            // For vorgaenge structure, collect zeitraeume from all vorgaenge
+            for (JsonNode vorgang : root.get("vorgaenge")) {
+                if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                    for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                        String id = zeitraum.path("id").asText("");
+                        String beginn = zeitraum.path("beginn").asText("");
+                        String ende = zeitraum.path("ende").asText("");
+                        result.add(new Z(id, beginn, ende));
+                    }
+                }
+            }
+        } else if (root.has("zeitraeume") && root.get("zeitraeume").isArray()) {
+            // Legacy single vorgang structure
+            for (JsonNode zeitraum : root.get("zeitraeume")) {
+                String id = zeitraum.path("id").asText("");
+                String beginn = zeitraum.path("beginn").asText("");
+                String ende = zeitraum.path("ende").asText("");
+                result.add(new Z(id, beginn, ende));
+            }
+        }
+        
+        return result;
+    }
+    
+    public JsonNode findZeitraumById(JsonNode root, String id) {
+        // Check if this is the new stammakte structure
+        if (root.has("stammakte")) {
+            JsonNode stammakte = root.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
+                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                            if (id.equals(zeitraum.path("id").asText(""))) {
+                                return zeitraum;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        // Check if this is the vorgaenge structure
+        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
+            // Search in all vorgaenge
+            for (JsonNode vorgang : root.get("vorgaenge")) {
+                if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                    for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                        if (id.equals(zeitraum.path("id").asText(""))) {
+                            return zeitraum;
+                        }
+                    }
+                }
+            }
+        } else if (root.has("zeitraeume") && root.get("zeitraeume").isArray()) {
+            // Legacy single vorgang structure
+            for (JsonNode zeitraum : root.get("zeitraeume")) {
+                if (id.equals(zeitraum.path("id").asText(""))) {
+                    return zeitraum;
+                }
+            }
+        }
+        return NullNode.getInstance();
+    }
+    
+    public JsonNode findStammvorgangByZeitraumId(JsonNode root, String zeitraumId) {
+        // Check if this is the new stammakte structure
+        if (root.has("stammakte")) {
+            JsonNode stammakte = root.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
+                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                            if (zeitraumId.equals(zeitraum.path("id").asText(""))) {
+                                return vorgang.path("stammvorgang");
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        // Check if this is the vorgaenge structure
+        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
+            // Search in all vorgaenge for the one containing this zeitraum
+            for (JsonNode vorgang : root.get("vorgaenge")) {
+                if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                    for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                        if (zeitraumId.equals(zeitraum.path("id").asText(""))) {
+                            return vorgang.path("stammvorgang");
+                        }
+                    }
+                }
+            }
+        } else {
+            // Legacy single vorgang structure - stammvorgang is at root level
+            return root.path("stammvorgang");
+        }
+        return NullNode.getInstance();
+    }
+    
+    public List<Vorgang> parseLeftCandidates(JsonNode left) {
+        List<Vorgang> result = new ArrayList<>();
+        
+        // Check if this is the new stammakte structure
+        if (left.has("stammakte")) {
+            JsonNode stammakte = left.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode v : stammakte.get("vorgaenge")) {
+                    JsonNode stammvorgang = v.path("stammvorgang");
+                    String id = stammvorgang.path("id").asText("");
+                    String vorgangstyp = stammvorgang.path("vorgangstyp").asText("");
+                    List<Z> zeitraeume = extractZeitraeume(JsonNodeFactory.instance.objectNode().set("vorgaenge", 
+                        JsonNodeFactory.instance.arrayNode().add(v)));
+                    result.add(new Vorgang(id, vorgangstyp, zeitraeume));
+                }
+            }
+        }
+        else if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
+            // a) wenn left.vorgaenge Array
+            for (JsonNode v : left.get("vorgaenge")) {
+                JsonNode stammvorgang = v.path("stammvorgang");
+                String id = stammvorgang.path("id").asText("");
+                String vorgangstyp = stammvorgang.path("vorgangstyp").asText("");
+                List<Z> zeitraeume = extractZeitraeume(JsonNodeFactory.instance.objectNode().set("vorgaenge", 
+                    JsonNodeFactory.instance.arrayNode().add(v)));
+                result.add(new Vorgang(id, vorgangstyp, zeitraeume));
+            }
+        } else {
+            // b) sonst: single vorgang
+            JsonNode stammvorgang = left.path("stammvorgang");
+            String id = stammvorgang.path("id").asText("");
+            String vorgangstyp = stammvorgang.path("vorgangstyp").asText("");
+            List<Z> zeitraeume = extractZeitraeume(left);
+            result.add(new Vorgang(id, vorgangstyp, zeitraeume));
+        }
+        
+        return result;
+    }
+    
+    public JsonNode getRootOfVorgang(JsonNode left, String vorgangId) {
+        // Check if this is the new stammakte structure
+        if (left.has("stammakte")) {
+            JsonNode stammakte = left.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode v : stammakte.get("vorgaenge")) {
+                    if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
+                        return v;
+                    }
+                }
+            }
+        }
+        else if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
+            for (JsonNode v : left.get("vorgaenge")) {
+                if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
+                    return v;
+                }
+            }
+        }
+        return left;
+    }
+    
+    public List<PersonRef> listRightPersons(JsonNode zeitraum) {
+        List<PersonRef> result = new ArrayList<>();
+        if (zeitraum != null && zeitraum.has("personen") && zeitraum.get("personen").isArray()) {
+            for (JsonNode personImZeitraum : zeitraum.get("personen")) {
+                JsonNode person = personImZeitraum.path("person");
+                String id = person.path("id").asText("");
+                String nachname = person.path("nachname").asText("");
+                String vorname = person.path("vorname").asText("");
+                String geburtsdatum = person.path("geburtsdatum").asText("");
+                
+                String label = nachname + ", " + vorname;
+                if (!geburtsdatum.isEmpty()) {
+                    label += " (" + FormatUtil.date(geburtsdatum) + ")";
+                }
+                
+                result.add(new PersonRef(id, label));
+            }
+        }
+        return result;
+    }
+    
+    public ArrayNode personsArray(JsonNode zeitraum) {
+        return zeitraum != null && zeitraum.has("personen") && zeitraum.get("personen").isArray()
+                ? (ArrayNode) zeitraum.get("personen")
+                : new ObjectMapper().createArrayNode();
+    }
+    
+    public StammvorgangInfo createStammvorgangInfo(JsonNode stammvorgangNode) {
+        if (stammvorgangNode == null || stammvorgangNode.isMissingNode()) {
+            return new StammvorgangInfo("", "", "");
+        }
+        
+        String id = stammvorgangNode.path("id").asText("");
+        String vorgangstyp = stammvorgangNode.path("vorgangstyp").asText("");
+        String vorgangsstatus = stammvorgangNode.path("vorgangsstatus").asText("");
+        
+        return new StammvorgangInfo(id, vorgangstyp, vorgangsstatus);
+    }
+
+    public StammakteInfo extractStammakteInfo(JsonNode root) {
+        JsonNode stammakte = root.path("stammakte");
+        if (stammakte.isMissingNode()) {
+            return new StammakteInfo("", "", "", "");
+        }
+        
+        String aktenzeichen = stammakte.path("aktenzeichen").asText("");
+        String antragstellername = stammakte.path("antragstellername").asText("");
+        String antragstellerGeburtsdatum = stammakte.path("antragstellerGeburtsdatum").asText("");
+        
+        JsonNode anschrift = stammakte.path("anschrift");
+        String anschriftStr = "";
+        if (!anschrift.isMissingNode()) {
+            StringBuilder sb = new StringBuilder();
+            String strasse = anschrift.path("strasse").asText("");
+            String hausnummer = anschrift.path("hausnummer").asText("");
+            String zusatz = anschrift.path("zusatz").asText("");
+            String plz = anschrift.path("plz").asText("");
+            String ort = anschrift.path("ort").asText("");
+            
+            if (!strasse.isEmpty()) sb.append(strasse);
+            if (!hausnummer.isEmpty()) sb.append(" ").append(hausnummer);
+            if (!zusatz.isEmpty()) sb.append(" ").append(zusatz);
+            if (!plz.isEmpty() || !ort.isEmpty()) {
+                if (sb.length() > 0) sb.append(", ");
+                if (!plz.isEmpty()) sb.append(plz);
+                if (!ort.isEmpty()) sb.append(" ").append(ort);
+            }
+            anschriftStr = sb.toString();
+        }
+        
+        return new StammakteInfo(aktenzeichen, antragstellername, antragstellerGeburtsdatum, anschriftStr);
+    }
+}

+ 442 - 0
src/main/java/com/tsystems/inwo/vgl/service/VergleichDataExtractor.java

@@ -0,0 +1,442 @@
+package com.tsystems.inwo.vgl.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.tsystems.inwo.vgl.view.EinkommenZeile;
+import com.tsystems.inwo.vgl.view.FreibetragZeile;
+import com.tsystems.inwo.vgl.view.KitaZeile;
+import com.tsystems.inwo.vgl.view.MitgliedRow;
+import com.tsystems.inwo.vgl.web.VergleichController.Pair;
+import com.tsystems.inwo.vgl.util.FormatUtil;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class VergleichDataExtractor {
+
+    private final DataComparisonService comparisonService;
+    private final HtmlRenderingService htmlService;
+
+    public VergleichDataExtractor(DataComparisonService comparisonService, HtmlRenderingService htmlService) {
+        this.comparisonService = comparisonService;
+        this.htmlService = htmlService;
+    }
+
+    public MitgliedRow buildMitgliedRow(String personId, JsonNode rightItem, JsonNode leftItem) {
+        // Extract person data from both items
+        JsonNode rightPerson = rightItem != null ? rightItem.path("person") : null;
+        JsonNode leftPerson = leftItem != null ? leftItem.path("person") : null;
+        
+        // Determine status and detect changes
+        String symbol = "";
+        String symbolClass = "";
+        String rowClass = "";
+        boolean hasChanges = false;
+        
+        if (rightItem != null && leftItem != null) {
+            // Both exist - check for changes
+            hasChanges = comparisonService.detectPersonChanges(rightItem, rightPerson, leftItem, leftPerson);
+            if (hasChanges) {
+                symbol = comparisonService.getSymbolFor("CHANGED");
+                symbolClass = comparisonService.getSymbolClassFor("CHANGED");
+                rowClass = "";
+            }
+        } else if (rightItem != null && leftItem == null) {
+            symbol = comparisonService.getSymbolFor("ADDED");
+            symbolClass = comparisonService.getSymbolClassFor("ADDED");
+            rowClass = comparisonService.getRowClassFor("ADDED");
+        } else if (rightItem == null && leftItem != null) {
+            symbol = comparisonService.getSymbolFor("REMOVED");
+            symbolClass = comparisonService.getSymbolClassFor("REMOVED");
+            rowClass = comparisonService.getRowClassFor("REMOVED");
+        }
+        
+        // Use right (current) data as primary, fall back to left for missing data
+        JsonNode primaryPerson = rightPerson != null ? rightPerson : leftPerson;
+        JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
+        
+        if (primaryPerson == null) {
+            return new MitgliedRow(
+                personId, symbol, symbolClass, rowClass, 
+                "", "", "", "", "", "", "", "", "", "", "",
+                "", "", "", "", "", "", "", "", "", ""
+            );
+        }
+        
+        // Extract data with change detection for HTML - Hauptfelder
+        String nameHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson, 
+            p -> p.path("nachname").asText("") + ", " + p.path("vorname").asText(""),
+            hasChanges
+        );
+        
+        String blocknameHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("blockname").asBoolean(false) ? "☑" : "☐",
+            hasChanges
+        );
+        
+        String geburtsdatumHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> !p.path("geburtsdatum").asText("").isEmpty() ? FormatUtil.date(p.path("geburtsdatum").asText("")) : "",
+            hasChanges
+        );
+        
+        String sterbedatumHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> !p.path("sterbedatum").asText("").isEmpty() ? FormatUtil.date(p.path("sterbedatum").asText("")) : "",
+            hasChanges
+        );
+        
+        String familienstandHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("familienstand").asText(""),
+            hasChanges
+        );
+        
+        String geschlechtHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("geschlecht").asText(""),
+            hasChanges
+        );
+        
+        String anredeHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("anrede").asText(""),
+            hasChanges
+        );
+        
+        // Item-level fields - angepasst an Schema
+        String verwandtschaftHtml = htmlService.buildItemFieldHtml(
+            rightItem, leftItem,
+            i -> i.path("verwandtschaftsverhaeltnis").asText(""),
+            hasChanges
+        );
+        
+        String ausschlussgrundHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("ausschlussgrund").asText(""),
+            hasChanges
+        );
+        
+        String sozialstellungHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("sozialstellung").asText(""),
+            hasChanges
+        );
+        
+        String aufenthaltstitelEndeHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> !p.path("aufenthaltstitelEnde").asText("").isEmpty() ? FormatUtil.date(p.path("aufenthaltstitelEnde").asText("")) : "",
+            hasChanges
+        );
+        
+        String antragstellerHtml = htmlService.buildItemFieldHtml(
+            rightItem, leftItem,
+            i -> i.path("rolleImHaushalt").asText("").equals("Antragsteller") ? "☑" : "☐",
+            hasChanges
+        );
+        
+        String zahlungsempfaengerHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("zahlungsempfaenger").asBoolean(false) ? "☑" : "☐",
+            hasChanges
+        );
+        
+        // Zusatzfelder
+        String titelHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("titel").asText(""),
+            hasChanges
+        );
+        
+        String geburtsortHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("geburtsort").asText(""),
+            hasChanges
+        );
+        
+        String telefonHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("telefon").asText(""),
+            hasChanges
+        );
+        
+        String aktenzeichenSozialHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("aktenzeichenSozial").asText(""),
+            hasChanges
+        );
+        
+        String geburtsnameHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("geburtsname").asText(""),
+            hasChanges
+        );
+        
+        String alterNameHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("alterName").asText(""),
+            hasChanges
+        );
+        
+        String emailHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("email").asText(""),
+            hasChanges
+        );
+        
+        String postabrutionsnummerHtml = htmlService.buildPersonFieldHtml(
+            rightPerson, leftPerson,
+            p -> p.path("postabrutionsnummer").asText(""),
+            hasChanges
+        );
+        
+        return new MitgliedRow(
+            personId, symbol, symbolClass, rowClass,
+            nameHtml, blocknameHtml, geburtsdatumHtml, sterbedatumHtml,
+            familienstandHtml, geschlechtHtml, anredeHtml,
+            verwandtschaftHtml, ausschlussgrundHtml, sozialstellungHtml, aufenthaltstitelEndeHtml,
+            antragstellerHtml, zahlungsempfaengerHtml,
+            titelHtml, geburtsortHtml, telefonHtml, aktenzeichenSozialHtml,
+            geburtsnameHtml, alterNameHtml, emailHtml, postabrutionsnummerHtml
+        );
+    }
+
+    public List<EinkommenZeile> buildEinkommenZeilen(JsonNode rightPerson, JsonNode leftPerson) {
+        // Extract einkommen arrays from both persons
+        ArrayNode rightEinkommen = rightPerson != null && rightPerson.has("einkommen") && rightPerson.get("einkommen").isArray()
+                ? (ArrayNode) rightPerson.get("einkommen")
+                : new ObjectMapper().createArrayNode();
+        ArrayNode leftEinkommen = leftPerson != null && leftPerson.has("einkommen") && leftPerson.get("einkommen").isArray()
+                ? (ArrayNode) leftPerson.get("einkommen")
+                : new ObjectMapper().createArrayNode();
+        
+        // UNION über einkommen[].id
+        Map<String, Pair<JsonNode, JsonNode>> einkommenPairs = comparisonService.unionByKey(
+                rightEinkommen, leftEinkommen, n -> n.path("id").asText()
+        );
+        
+        List<EinkommenZeile> result = new ArrayList<>();
+        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : einkommenPairs.entrySet()) {
+            JsonNode rightItem = entry.getValue().right;
+            JsonNode leftItem = entry.getValue().left;
+            
+            // Determine status and changes
+            String symbol = "";
+            String symbolClass = "";
+            String rowClass = "";
+            boolean hasChanges = false;
+            
+            if (rightItem != null && leftItem != null) {
+                hasChanges = comparisonService.detectEinkommenChanges(rightItem, leftItem);
+                if (hasChanges) {
+                    symbol = comparisonService.getSymbolFor("CHANGED");
+                    symbolClass = comparisonService.getSymbolClassFor("CHANGED");
+                }
+            } else if (rightItem != null && leftItem == null) {
+                symbol = comparisonService.getSymbolFor("ADDED");
+                symbolClass = comparisonService.getSymbolClassFor("ADDED");
+                rowClass = comparisonService.getRowClassFor("ADDED");
+            } else if (rightItem == null && leftItem != null) {
+                symbol = comparisonService.getSymbolFor("REMOVED");
+                symbolClass = comparisonService.getSymbolClassFor("REMOVED");
+                rowClass = comparisonService.getRowClassFor("REMOVED");
+            }
+            
+            // Use primary data (right first, then left)
+            JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
+            
+            if (primaryItem != null) {
+                String artHtml = htmlService.buildEinkommenFieldHtml(rightItem, leftItem, 
+                    i -> {
+                        String art = i.path("art").asText("");
+                        String artZusatz = i.path("artZusatz").asText("");
+                        return !artZusatz.isEmpty() ? art + " (" + artZusatz + ")" : art;
+                    }, hasChanges);
+                
+                String faktorHtml = htmlService.buildEinkommenFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double faktor = i.path("faktor").asDouble(12);
+                        return faktor == 12 ? "monatlich" : 
+                               faktor == 1 ? "jährlich" : 
+                               String.format("%.0fx/Jahr", faktor);
+                    }, hasChanges);
+                
+                String betragHtml = htmlService.buildEinkommenFieldHtml(rightItem, leftItem,
+                    i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
+                
+                String zuschlagHtml = htmlService.buildEinkommenFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double zuschlag = i.path("zuschlag").asDouble(0.0);
+                        return zuschlag > 0 ? String.format("%.2f €", zuschlag) : "";
+                    }, hasChanges);
+                
+                String abschlagHtml = htmlService.buildEinkommenFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double abschlag = i.path("abschlag").asDouble(0.0);
+                        return abschlag > 0 ? String.format("%.2f €", abschlag) : "";
+                    }, hasChanges);
+                
+                String werbungskostenHtml = htmlService.buildEinkommenFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double werbungskosten = i.path("werbungskosten").asDouble(0.0);
+                        return werbungskosten > 0 ? String.format("%.2f €", werbungskosten) : "";
+                    }, hasChanges);
+                
+                result.add(new EinkommenZeile(
+                    symbol, symbolClass, rowClass,
+                    artHtml, faktorHtml, betragHtml,
+                    zuschlagHtml, abschlagHtml, werbungskostenHtml
+                ));
+            }
+        }
+        
+        return result;
+    }
+
+    public List<FreibetragZeile> buildFreibetragZeilen(JsonNode rightPerson, JsonNode leftPerson) {
+        // Extract freibetraege arrays from both persons
+        ArrayNode rightFreibetraege = rightPerson != null && rightPerson.has("freibetraege") && rightPerson.get("freibetraege").isArray()
+                ? (ArrayNode) rightPerson.get("freibetraege")
+                : new ObjectMapper().createArrayNode();
+        ArrayNode leftFreibetraege = leftPerson != null && leftPerson.has("freibetraege") && leftPerson.get("freibetraege").isArray()
+                ? (ArrayNode) leftPerson.get("freibetraege")
+                : new ObjectMapper().createArrayNode();
+        
+        // UNION über freibetraege[].id
+        Map<String, Pair<JsonNode, JsonNode>> freibetragPairs = comparisonService.unionByKey(
+                rightFreibetraege, leftFreibetraege, n -> n.path("id").asText()
+        );
+        
+        List<FreibetragZeile> result = new ArrayList<>();
+        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : freibetragPairs.entrySet()) {
+            JsonNode rightItem = entry.getValue().right;
+            JsonNode leftItem = entry.getValue().left;
+            
+            // Determine status and changes
+            String symbol = "";
+            String symbolClass = "";
+            String rowClass = "";
+            boolean hasChanges = false;
+            
+            if (rightItem != null && leftItem != null) {
+                hasChanges = comparisonService.detectFreibetragChanges(rightItem, leftItem);
+                if (hasChanges) {
+                    symbol = comparisonService.getSymbolFor("CHANGED");
+                    symbolClass = comparisonService.getSymbolClassFor("CHANGED");
+                }
+            } else if (rightItem != null && leftItem == null) {
+                symbol = comparisonService.getSymbolFor("ADDED");
+                symbolClass = comparisonService.getSymbolClassFor("ADDED");
+                rowClass = comparisonService.getRowClassFor("ADDED");
+            } else if (rightItem == null && leftItem != null) {
+                symbol = comparisonService.getSymbolFor("REMOVED");
+                symbolClass = comparisonService.getSymbolClassFor("REMOVED");
+                rowClass = comparisonService.getRowClassFor("REMOVED");
+            }
+            
+            // Use primary data (right first, then left)
+            JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
+            
+            if (primaryItem != null) {
+                String artHtml = htmlService.buildFreibetragFieldHtml(rightItem, leftItem,
+                    i -> i.path("art").asText(""), hasChanges);
+                
+                String frequenzHtml = htmlService.buildFreibetragFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double faktor = i.path("faktor").asDouble(12);
+                        return faktor == 12 ? "monatlich" : 
+                               faktor == 1 ? "jährlich" : 
+                               String.format("%.0fx/Jahr", faktor);
+                    }, hasChanges);
+                
+                String betragHtml = htmlService.buildFreibetragFieldHtml(rightItem, leftItem,
+                    i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
+                
+                String fristHtml = htmlService.buildFreibetragFieldHtml(rightItem, leftItem,
+                    i -> {
+                        String fristDatum = i.path("fristDatum").asText("");
+                        return !fristDatum.isEmpty() ? FormatUtil.date(fristDatum) : "";
+                    }, hasChanges);
+                
+                String typ = primaryItem.path("typ").asText("F"); // Default to "F" if not specified
+                
+                result.add(new FreibetragZeile(
+                    symbol, symbolClass, rowClass,
+                    typ, artHtml, frequenzHtml, betragHtml, fristHtml
+                ));
+            }
+        }
+        
+        return result;
+    }
+
+    public List<KitaZeile> buildKitaZeilen(JsonNode rightPerson, JsonNode leftPerson, Map<String, String> personNameByIdRight) {
+        // Extract kinderbetreuung arrays from both persons
+        ArrayNode rightKinderbetreuung = rightPerson != null && rightPerson.has("kinderbetreuung") && rightPerson.get("kinderbetreuung").isArray()
+                ? (ArrayNode) rightPerson.get("kinderbetreuung")
+                : new ObjectMapper().createArrayNode();
+        ArrayNode leftKinderbetreuung = leftPerson != null && leftPerson.has("kinderbetreuung") && leftPerson.get("kinderbetreuung").isArray()
+                ? (ArrayNode) leftPerson.get("kinderbetreuung")
+                : new ObjectMapper().createArrayNode();
+        
+        // UNION über kinderbetreuung[].id
+        Map<String, Pair<JsonNode, JsonNode>> kitaPairs = comparisonService.unionByKey(
+                rightKinderbetreuung, leftKinderbetreuung, n -> n.path("id").asText()
+        );
+        
+        List<KitaZeile> result = new ArrayList<>();
+        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : kitaPairs.entrySet()) {
+            JsonNode rightItem = entry.getValue().right;
+            JsonNode leftItem = entry.getValue().left;
+            
+            // Determine status and changes
+            String symbol = "";
+            String symbolClass = "";
+            String rowClass = "";
+            boolean hasChanges = false;
+            
+            if (rightItem != null && leftItem != null) {
+                hasChanges = comparisonService.detectKitaChanges(rightItem, leftItem);
+                if (hasChanges) {
+                    symbol = comparisonService.getSymbolFor("CHANGED");
+                    symbolClass = comparisonService.getSymbolClassFor("CHANGED");
+                }
+            } else if (rightItem != null && leftItem == null) {
+                symbol = comparisonService.getSymbolFor("ADDED");
+                symbolClass = comparisonService.getSymbolClassFor("ADDED");
+                rowClass = comparisonService.getRowClassFor("ADDED");
+            } else if (rightItem == null && leftItem != null) {
+                symbol = comparisonService.getSymbolFor("REMOVED");
+                symbolClass = comparisonService.getSymbolClassFor("REMOVED");
+                rowClass = comparisonService.getRowClassFor("REMOVED");
+            }
+            
+            // Use primary data (right first, then left)
+            JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
+            
+            if (primaryItem != null) {
+                String kindNameHtml = htmlService.buildKitaFieldHtml(rightItem, leftItem,
+                    i -> {
+                        String kindRef = i.path("kindRef").asText("");
+                        return personNameByIdRight.getOrDefault(kindRef, "Kind ID: " + kindRef);
+                    }, hasChanges);
+                
+                String betragHtml = htmlService.buildKitaFieldHtml(rightItem, leftItem,
+                    i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
+                
+                result.add(new KitaZeile(
+                    symbol, symbolClass, rowClass,
+                    kindNameHtml, betragHtml
+                ));
+            }
+        }
+        
+        return result;
+    }
+}

+ 30 - 867
src/main/java/com/tsystems/inwo/vgl/web/VergleichController.java

@@ -2,9 +2,9 @@ package com.tsystems.inwo.vgl.web;
 
 import com.fasterxml.jackson.databind.*;
 import com.fasterxml.jackson.databind.node.*;
-import com.tsystems.inwo.vgl.service.SchemaValidationService;
-import com.tsystems.inwo.vgl.util.FormatUtil;
+import com.tsystems.inwo.vgl.service.*;
 import com.tsystems.inwo.vgl.view.*;
+import com.tsystems.inwo.vgl.util.FormatUtil;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.*;
@@ -16,10 +16,19 @@ import java.util.stream.Collectors;
 public class VergleichController {
     private final ObjectMapper om;
     private final SchemaValidationService validator;
-    
-    public VergleichController(ObjectMapper om, SchemaValidationService v) {
+    private final JsonNavigationService jsonNavigationService;
+    private final DataComparisonService comparisonService;
+    private final VergleichDataExtractor dataExtractor;
+    
+    public VergleichController(ObjectMapper om, SchemaValidationService validator, 
+                              JsonNavigationService jsonNavigationService,
+                              DataComparisonService comparisonService,
+                              VergleichDataExtractor dataExtractor) {
         this.om = om;
-        this.validator = v;
+        this.validator = validator;
+        this.jsonNavigationService = jsonNavigationService;
+        this.comparisonService = comparisonService;
+        this.dataExtractor = dataExtractor;
     }
 
     @GetMapping("/test")
@@ -72,20 +81,20 @@ public class VergleichController {
         }
 
         // --- Rechts (Master) Zeitraum wählen ---
-        List<Z> zeitrechte = extractZeitraeume(right);
+        List<Z> zeitrechte = jsonNavigationService.extractZeitraeume(right);
         if (zeitrechte.isEmpty()) throw new IllegalArgumentException("Right JSON hat keine Zeiträume");
         if (rightZeitraumId == null) rightZeitraumId = zeitrechte.get(0).id;
-        JsonNode rightZeitraum = findZeitraumById(right, rightZeitraumId);
+        JsonNode rightZeitraum = jsonNavigationService.findZeitraumById(right, rightZeitraumId);
 
         // --- Find the stammvorgang that contains the selected zeitraum ---
-        JsonNode stammvorgangJsonNode = findStammvorgangByZeitraumId(right, rightZeitraumId);
-        StammvorgangInfo stammvorgangRight = createStammvorgangInfo(stammvorgangJsonNode);
+        JsonNode stammvorgangJsonNode = jsonNavigationService.findStammvorgangByZeitraumId(right, rightZeitraumId);
+        StammvorgangInfo stammvorgangRight = jsonNavigationService.createStammvorgangInfo(stammvorgangJsonNode);
 
         // --- Extract stammakte info if available ---
-        StammakteInfo stammakteRight = extractStammakteInfo(right);
+        StammakteInfo stammakteRight = jsonNavigationService.extractStammakteInfo(right);
 
         // --- Links (Baseline) Kandidaten & Auswahl ---
-        List<Vorgang> leftCand = parseLeftCandidates(left);
+        List<Vorgang> leftCand = jsonNavigationService.parseLeftCandidates(left);
         List<Z> leftZ = List.of();
         JsonNode leftZeitraum = null;
         if (!leftCand.isEmpty()) {
@@ -93,19 +102,21 @@ public class VergleichController {
             final String finalLeftVorgangId = leftVorgangId;
             leftZ = leftCand.stream().filter(v -> v.id.equals(finalLeftVorgangId)).findFirst().orElse(leftCand.get(0)).zeitraeume;
             if (leftZeitraumId == null && !leftZ.isEmpty()) leftZeitraumId = leftZ.get(0).id;
-            if (leftZeitraumId != null) leftZeitraum = findZeitraumById(getRootOfVorgang(left, leftVorgangId), leftZeitraumId);
+            if (leftZeitraumId != null) leftZeitraum = jsonNavigationService.findZeitraumById(jsonNavigationService.getRootOfVorgang(left, leftVorgangId), leftZeitraumId);
         }
 
         // --- Personenliste rechts + Default personId ---
-        List<PersonRef> personenRight = listRightPersons(rightZeitraum);
+        List<PersonRef> personenRight = jsonNavigationService.listRightPersons(rightZeitraum);
         if (personId == null && !personenRight.isEmpty()) personId = personenRight.get(0).id;
 
         // --- Personen-Tabelle (UNION via person.id) ---
-        Map<String, Pair<JsonNode, JsonNode>> personenPairs = unionByKey(
-                personsArray(rightZeitraum), personsArray(leftZeitraum), n -> n.path("person").path("id").asText()
+        Map<String, Pair<JsonNode, JsonNode>> personenPairs = comparisonService.unionByKey(
+                jsonNavigationService.personsArray(rightZeitraum), 
+                jsonNavigationService.personsArray(leftZeitraum), 
+                n -> n.path("person").path("id").asText()
         );
         List<MitgliedRow> mitgliederRows = personenPairs.entrySet().stream()
-                .map(e -> buildMitgliedRow(e.getKey(), e.getValue().right, e.getValue().left))
+                .map(e -> dataExtractor.buildMitgliedRow(e.getKey(), e.getValue().right, e.getValue().left))
                 .sorted(Comparator.comparing(MitgliedRow::personId))
                 .toList();
 
@@ -119,9 +130,9 @@ public class VergleichController {
         Map<String, String> personNameByIdRight = personenRight.stream().collect(Collectors.toMap(p -> p.id, p -> p.label));
 
         // Einkommen / Freibeträge / Kita: jeweils UNION per id
-        List<EinkommenZeile> einkommenZeilen = buildEinkommenZeilen(rightPerson, leftPerson);
-        List<FreibetragZeile> freibetragZeilen = buildFreibetragZeilen(rightPerson, leftPerson);
-        List<KitaZeile> kitaZeilen = buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
+        List<EinkommenZeile> einkommenZeilen = dataExtractor.buildEinkommenZeilen(rightPerson, leftPerson);
+        List<FreibetragZeile> freibetragZeilen = dataExtractor.buildFreibetragZeilen(rightPerson, leftPerson);
+        List<KitaZeile> kitaZeilen = dataExtractor.buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
 
         // --- Model füllen & rendern ---
         model.addAttribute("leftJson", leftJson);
@@ -161,852 +172,4 @@ public class VergleichController {
             left = l;
         }
     }
-
-    // ===== Helper-Methoden =====
-    private static StammvorgangInfo createStammvorgangInfo(JsonNode stammvorgangNode) {
-        if (stammvorgangNode == null || stammvorgangNode.isMissingNode()) {
-            return new StammvorgangInfo("", "", "");
-        }
-        
-        String id = stammvorgangNode.path("id").asText("");
-        String vorgangstyp = stammvorgangNode.path("vorgangstyp").asText("");
-        String vorgangsstatus = stammvorgangNode.path("vorgangsstatus").asText("");
-        
-        return new StammvorgangInfo(id, vorgangstyp, vorgangsstatus);
-    }
-
-    private static StammakteInfo extractStammakteInfo(JsonNode root) {
-        JsonNode stammakte = root.path("stammakte");
-        if (stammakte.isMissingNode()) {
-            return new StammakteInfo("", "", "", "");
-        }
-        
-        String aktenzeichen = stammakte.path("aktenzeichen").asText("");
-        String antragstellername = stammakte.path("antragstellername").asText("");
-        String antragstellerGeburtsdatum = stammakte.path("antragstellerGeburtsdatum").asText("");
-        
-        JsonNode anschrift = stammakte.path("anschrift");
-        String anschriftStr = "";
-        if (!anschrift.isMissingNode()) {
-            StringBuilder sb = new StringBuilder();
-            String strasse = anschrift.path("strasse").asText("");
-            String hausnummer = anschrift.path("hausnummer").asText("");
-            String zusatz = anschrift.path("zusatz").asText("");
-            String plz = anschrift.path("plz").asText("");
-            String ort = anschrift.path("ort").asText("");
-            
-            if (!strasse.isEmpty()) sb.append(strasse);
-            if (!hausnummer.isEmpty()) sb.append(" ").append(hausnummer);
-            if (!zusatz.isEmpty()) sb.append(" ").append(zusatz);
-            if (!plz.isEmpty() || !ort.isEmpty()) {
-                if (sb.length() > 0) sb.append(", ");
-                if (!plz.isEmpty()) sb.append(plz);
-                if (!ort.isEmpty()) sb.append(" ").append(ort);
-            }
-            anschriftStr = sb.toString();
-        }
-        
-        return new StammakteInfo(aktenzeichen, antragstellername, antragstellerGeburtsdatum, anschriftStr);
-    }
-
-    private static List<Z> extractZeitraeume(JsonNode root) {
-        List<Z> result = new ArrayList<>();
-        
-        // Check if this is the new stammakte structure
-        if (root.has("stammakte")) {
-            JsonNode stammakte = root.get("stammakte");
-            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
-                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
-                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
-                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
-                            String id = zeitraum.path("id").asText("");
-                            String beginn = zeitraum.path("beginn").asText("");
-                            String ende = zeitraum.path("ende").asText("");
-                            result.add(new Z(id, beginn, ende));
-                        }
-                    }
-                }
-            }
-        }
-        // Check if this is the vorgaenge structure
-        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
-            // For vorgaenge structure, collect zeitraeume from all vorgaenge
-            for (JsonNode vorgang : root.get("vorgaenge")) {
-                if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
-                    for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
-                        String id = zeitraum.path("id").asText("");
-                        String beginn = zeitraum.path("beginn").asText("");
-                        String ende = zeitraum.path("ende").asText("");
-                        result.add(new Z(id, beginn, ende));
-                    }
-                }
-            }
-        } else if (root.has("zeitraeume") && root.get("zeitraeume").isArray()) {
-            // Legacy single vorgang structure
-            for (JsonNode zeitraum : root.get("zeitraeume")) {
-                String id = zeitraum.path("id").asText("");
-                String beginn = zeitraum.path("beginn").asText("");
-                String ende = zeitraum.path("ende").asText("");
-                result.add(new Z(id, beginn, ende));
-            }
-        }
-        
-        return result;
-    }
-    
-    private static JsonNode findZeitraumById(JsonNode root, String id) {
-        // Check if this is the new stammakte structure
-        if (root.has("stammakte")) {
-            JsonNode stammakte = root.get("stammakte");
-            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
-                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
-                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
-                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
-                            if (id.equals(zeitraum.path("id").asText(""))) {
-                                return zeitraum;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        // Check if this is the vorgaenge structure
-        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
-            // Search in all vorgaenge
-            for (JsonNode vorgang : root.get("vorgaenge")) {
-                if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
-                    for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
-                        if (id.equals(zeitraum.path("id").asText(""))) {
-                            return zeitraum;
-                        }
-                    }
-                }
-            }
-        } else if (root.has("zeitraeume") && root.get("zeitraeume").isArray()) {
-            // Legacy single vorgang structure
-            for (JsonNode zeitraum : root.get("zeitraeume")) {
-                if (id.equals(zeitraum.path("id").asText(""))) {
-                    return zeitraum;
-                }
-            }
-        }
-        return NullNode.getInstance();
-    }
-    
-    private static JsonNode findStammvorgangByZeitraumId(JsonNode root, String zeitraumId) {
-        // Check if this is the new stammakte structure
-        if (root.has("stammakte")) {
-            JsonNode stammakte = root.get("stammakte");
-            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
-                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
-                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
-                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
-                            if (zeitraumId.equals(zeitraum.path("id").asText(""))) {
-                                return vorgang.path("stammvorgang");
-                            }
-                        }
-                    }
-                }
-            }
-        }
-        // Check if this is the vorgaenge structure
-        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
-            // Search in all vorgaenge for the one containing this zeitraum
-            for (JsonNode vorgang : root.get("vorgaenge")) {
-                if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
-                    for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
-                        if (zeitraumId.equals(zeitraum.path("id").asText(""))) {
-                            return vorgang.path("stammvorgang");
-                        }
-                    }
-                }
-            }
-        } else {
-            // Legacy single vorgang structure - stammvorgang is at root level
-            return root.path("stammvorgang");
-        }
-        return NullNode.getInstance();
-    }
-    
-    private static List<Vorgang> parseLeftCandidates(JsonNode left) {
-        List<Vorgang> result = new ArrayList<>();
-        
-        // Check if this is the new stammakte structure
-        if (left.has("stammakte")) {
-            JsonNode stammakte = left.get("stammakte");
-            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
-                for (JsonNode v : stammakte.get("vorgaenge")) {
-                    JsonNode stammvorgang = v.path("stammvorgang");
-                    String id = stammvorgang.path("id").asText("");
-                    String vorgangstyp = stammvorgang.path("vorgangstyp").asText("");
-                    List<Z> zeitraeume = extractZeitraeume(JsonNodeFactory.instance.objectNode().set("vorgaenge", 
-                        JsonNodeFactory.instance.arrayNode().add(v)));
-                    result.add(new Vorgang(id, vorgangstyp, zeitraeume));
-                }
-            }
-        }
-        else if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
-            // a) wenn left.vorgaenge Array
-            for (JsonNode v : left.get("vorgaenge")) {
-                JsonNode stammvorgang = v.path("stammvorgang");
-                String id = stammvorgang.path("id").asText("");
-                String vorgangstyp = stammvorgang.path("vorgangstyp").asText("");
-                List<Z> zeitraeume = extractZeitraeume(JsonNodeFactory.instance.objectNode().set("vorgaenge", 
-                    JsonNodeFactory.instance.arrayNode().add(v)));
-                result.add(new Vorgang(id, vorgangstyp, zeitraeume));
-            }
-        } else {
-            // b) sonst: single vorgang
-            JsonNode stammvorgang = left.path("stammvorgang");
-            String id = stammvorgang.path("id").asText("");
-            String vorgangstyp = stammvorgang.path("vorgangstyp").asText("");
-            List<Z> zeitraeume = extractZeitraeume(left);
-            result.add(new Vorgang(id, vorgangstyp, zeitraeume));
-        }
-        
-        return result;
-    }
-    
-    private static JsonNode getRootOfVorgang(JsonNode left, String vorgangId) {
-        // Check if this is the new stammakte structure
-        if (left.has("stammakte")) {
-            JsonNode stammakte = left.get("stammakte");
-            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
-                for (JsonNode v : stammakte.get("vorgaenge")) {
-                    if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
-                        return v;
-                    }
-                }
-            }
-        }
-        else if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
-            for (JsonNode v : left.get("vorgaenge")) {
-                if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
-                    return v;
-                }
-            }
-        }
-        return left;
-    }
-    
-    private static List<PersonRef> listRightPersons(JsonNode zeitraum) {
-        List<PersonRef> result = new ArrayList<>();
-        if (zeitraum != null && zeitraum.has("personen") && zeitraum.get("personen").isArray()) {
-            for (JsonNode personImZeitraum : zeitraum.get("personen")) {
-                JsonNode person = personImZeitraum.path("person");
-                String id = person.path("id").asText("");
-                String nachname = person.path("nachname").asText("");
-                String vorname = person.path("vorname").asText("");
-                String geburtsdatum = person.path("geburtsdatum").asText("");
-                
-                String label = nachname + ", " + vorname;
-                if (!geburtsdatum.isEmpty()) {
-                    label += " (" + FormatUtil.date(geburtsdatum) + ")";
-                }
-                
-                result.add(new PersonRef(id, label));
-            }
-        }
-        return result;
-    }
-    
-    private static ArrayNode personsArray(JsonNode zeitraum) {
-        return zeitraum != null && zeitraum.has("personen") && zeitraum.get("personen").isArray()
-                ? (ArrayNode) zeitraum.get("personen")
-                : new ObjectMapper().createArrayNode();
-    }
-    
-    private static <T> Map<String, Pair<JsonNode, JsonNode>> unionByKey(ArrayNode rightArr, ArrayNode leftArr, Function<JsonNode, String> keyFn) {
-        Map<String, Pair<JsonNode, JsonNode>> result = new LinkedHashMap<>();
-        
-        // Add all right items
-        for (JsonNode node : rightArr) {
-            String key = keyFn.apply(node);
-            result.put(key, new Pair<>(node, null));
-        }
-        
-        // Merge left items
-        for (JsonNode node : leftArr) {
-            String key = keyFn.apply(node);
-            result.merge(key, new Pair<>(null, node), (existing, newPair) -> 
-                new Pair<>(existing.right, newPair.left));
-        }
-        
-        return result;
-    }
-    
-    private static MitgliedRow buildMitgliedRow(String personId, JsonNode rightItem, JsonNode leftItem) {
-        // Extract person data from both items
-        JsonNode rightPerson = rightItem != null ? rightItem.path("person") : null;
-        JsonNode leftPerson = leftItem != null ? leftItem.path("person") : null;
-        
-        // Determine status and detect changes
-        String symbol = "";
-        String symbolClass = "";
-        String rowClass = "";
-        boolean hasChanges = false;
-        
-        if (rightItem != null && leftItem != null) {
-            // Both exist - check for changes
-            hasChanges = detectPersonChanges(rightItem, rightPerson, leftItem, leftPerson);
-            if (hasChanges) {
-                symbol = symFor("CHANGED");
-                symbolClass = symClassFor("CHANGED");
-                rowClass = "";
-            }
-        } else if (rightItem != null && leftItem == null) {
-            symbol = symFor("ADDED");
-            symbolClass = symClassFor("ADDED");
-            rowClass = rowClassFor("ADDED");
-        } else if (rightItem == null && leftItem != null) {
-            symbol = symFor("REMOVED");
-            symbolClass = symClassFor("REMOVED");
-            rowClass = rowClassFor("REMOVED");
-        }
-        
-        // Use right (current) data as primary, fall back to left for missing data
-        JsonNode primaryPerson = rightPerson != null ? rightPerson : leftPerson;
-        JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
-        
-        if (primaryPerson == null) {
-            return new MitgliedRow(
-                personId, symbol, symbolClass, rowClass, 
-                "", "", "", "", "", "", "", "", "", "", "",
-                "", "", "", "", "", "", "", "", "", ""
-            );
-        }
-        
-        // Extract data with change detection for HTML - Hauptfelder
-        String nameHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson, 
-            p -> p.path("nachname").asText("") + ", " + p.path("vorname").asText(""),
-            hasChanges
-        );
-        
-        String blocknameHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("blockname").asBoolean(false) ? "☑" : "☐",
-            hasChanges
-        );
-        
-        String geburtsdatumHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> !p.path("geburtsdatum").asText("").isEmpty() ? FormatUtil.date(p.path("geburtsdatum").asText("")) : "",
-            hasChanges
-        );
-        
-        String sterbedatumHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> !p.path("sterbedatum").asText("").isEmpty() ? FormatUtil.date(p.path("sterbedatum").asText("")) : "",
-            hasChanges
-        );
-        
-        String familienstandHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("familienstand").asText(""),
-            hasChanges
-        );
-        
-        String geschlechtHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("geschlecht").asText(""),
-            hasChanges
-        );
-        
-        String anredeHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("anrede").asText(""),
-            hasChanges
-        );
-        
-        // Item-level fields - angepasst an Schema
-        String verwandtschaftHtml = buildItemFieldHtml(
-            rightItem, leftItem,
-            i -> i.path("verwandtschaftsverhaeltnis").asText(""),
-            hasChanges
-        );
-        
-        String ausschlussgrundHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("ausschlussgrund").asText(""),
-            hasChanges
-        );
-        
-        String sozialstellungHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("sozialstellung").asText(""),
-            hasChanges
-        );
-        
-        String aufenthaltstitelEndeHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> !p.path("aufenthaltstitelEnde").asText("").isEmpty() ? FormatUtil.date(p.path("aufenthaltstitelEnde").asText("")) : "",
-            hasChanges
-        );
-        
-        String antragstellerHtml = buildItemFieldHtml(
-            rightItem, leftItem,
-            i -> i.path("rolleImHaushalt").asText("").equals("Antragsteller") ? "☑" : "☐",
-            hasChanges
-        );
-        
-        String zahlungsempfaengerHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("zahlungsempfaenger").asBoolean(false) ? "☑" : "☐",
-            hasChanges
-        );
-        
-        // Zusatzfelder
-        String titelHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("titel").asText(""),
-            hasChanges
-        );
-        
-        String geburtsortHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("geburtsort").asText(""),
-            hasChanges
-        );
-        
-        String telefonHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("telefon").asText(""),
-            hasChanges
-        );
-        
-        String aktenzeichenSozialHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("aktenzeichenSozial").asText(""),
-            hasChanges
-        );
-        
-        String geburtsnameHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("geburtsname").asText(""),
-            hasChanges
-        );
-        
-        String alterNameHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("alterName").asText(""),
-            hasChanges
-        );
-        
-        String emailHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("email").asText(""),
-            hasChanges
-        );
-        
-        String postabrutionsnummerHtml = buildPersonFieldHtml(
-            rightPerson, leftPerson,
-            p -> p.path("postabrutionsnummer").asText(""),
-            hasChanges
-        );
-        
-        return new MitgliedRow(
-            personId, symbol, symbolClass, rowClass,
-            nameHtml, blocknameHtml, geburtsdatumHtml, sterbedatumHtml,
-            familienstandHtml, geschlechtHtml, anredeHtml,
-            verwandtschaftHtml, ausschlussgrundHtml, sozialstellungHtml, aufenthaltstitelEndeHtml,
-            antragstellerHtml, zahlungsempfaengerHtml,
-            titelHtml, geburtsortHtml, telefonHtml, aktenzeichenSozialHtml,
-            geburtsnameHtml, alterNameHtml, emailHtml, postabrutionsnummerHtml
-        );
-    }
-    
-    private static boolean detectPersonChanges(JsonNode rightItem, JsonNode rightPerson, JsonNode leftItem, JsonNode leftPerson) {
-        if (rightPerson == null || leftPerson == null) return false;
-        
-        // Check person fields
-        return !Objects.equals(rightPerson.path("nachname").asText(""), leftPerson.path("nachname").asText("")) ||
-               !Objects.equals(rightPerson.path("vorname").asText(""), leftPerson.path("vorname").asText("")) ||
-               !Objects.equals(rightPerson.path("geburtsdatum").asText(""), leftPerson.path("geburtsdatum").asText("")) ||
-               !Objects.equals(rightPerson.path("familienstand").asText(""), leftPerson.path("familienstand").asText("")) ||
-               !Objects.equals(rightPerson.path("geschlecht").asText(""), leftPerson.path("geschlecht").asText("")) ||
-               !Objects.equals(rightItem.path("verwandtschaftsverhaeltnis").asText(""), leftItem.path("verwandtschaftsverhaeltnis").asText("")) ||
-               !Objects.equals(rightPerson.path("sozialstellung").asText(""), leftPerson.path("sozialstellung").asText("")) ||
-               !Objects.equals(rightItem.path("rolleImHaushalt").asText(""), leftItem.path("rolleImHaushalt").asText("")) ||
-               rightPerson.path("zahlungsempfaenger").asBoolean(false) != leftPerson.path("zahlungsempfaenger").asBoolean(false);
-    }
-    
-    private static String buildPersonFieldHtml(JsonNode rightPerson, JsonNode leftPerson, Function<JsonNode, String> extractor, boolean hasChanges) {
-        if (!hasChanges || rightPerson == null) {
-            JsonNode primary = rightPerson != null ? rightPerson : leftPerson;
-            return primary != null ? escape(extractor.apply(primary)) : "";
-        }
-        
-        String rightVal = rightPerson != null ? extractor.apply(rightPerson) : "";
-        String leftVal = leftPerson != null ? extractor.apply(leftPerson) : "";
-        
-        if (!Objects.equals(rightVal, leftVal)) {
-            return cellHtml(rightVal, leftVal, true);
-        } else {
-            return escape(rightVal);
-        }
-    }
-    
-    private static String buildItemFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
-        if (!hasChanges || rightItem == null) {
-            JsonNode primary = rightItem != null ? rightItem : leftItem;
-            return primary != null ? escape(extractor.apply(primary)) : "";
-        }
-        
-        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
-        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
-        
-        if (!Objects.equals(rightVal, leftVal)) {
-            return cellHtml(rightVal, leftVal, true);
-        } else {
-            return escape(rightVal);
-        }
-    }
-    
-    private static List<EinkommenZeile> buildEinkommenZeilen(JsonNode rightPerson, JsonNode leftPerson) {
-        // Extract einkommen arrays from both persons
-        ArrayNode rightEinkommen = rightPerson != null && rightPerson.has("einkommen") && rightPerson.get("einkommen").isArray()
-                ? (ArrayNode) rightPerson.get("einkommen")
-                : new ObjectMapper().createArrayNode();
-        ArrayNode leftEinkommen = leftPerson != null && leftPerson.has("einkommen") && leftPerson.get("einkommen").isArray()
-                ? (ArrayNode) leftPerson.get("einkommen")
-                : new ObjectMapper().createArrayNode();
-        
-        // UNION über einkommen[].id
-        Map<String, Pair<JsonNode, JsonNode>> einkommenPairs = unionByKey(
-                rightEinkommen, leftEinkommen, n -> n.path("id").asText()
-        );
-        
-        List<EinkommenZeile> result = new ArrayList<>();
-        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : einkommenPairs.entrySet()) {
-            JsonNode rightItem = entry.getValue().right;
-            JsonNode leftItem = entry.getValue().left;
-            
-            // Determine status and changes
-            String symbol = "";
-            String symbolClass = "";
-            String rowClass = "";
-            boolean hasChanges = false;
-            
-            if (rightItem != null && leftItem != null) {
-                hasChanges = detectEinkommenChanges(rightItem, leftItem);
-                if (hasChanges) {
-                    symbol = symFor("CHANGED");
-                    symbolClass = symClassFor("CHANGED");
-                }
-            } else if (rightItem != null && leftItem == null) {
-                symbol = symFor("ADDED");
-                symbolClass = symClassFor("ADDED");
-                rowClass = rowClassFor("ADDED");
-            } else if (rightItem == null && leftItem != null) {
-                symbol = symFor("REMOVED");
-                symbolClass = symClassFor("REMOVED");
-                rowClass = rowClassFor("REMOVED");
-            }
-            
-            // Use primary data (right first, then left)
-            JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
-            
-            if (primaryItem != null) {
-                String artHtml = buildEinkommenFieldHtml(rightItem, leftItem, 
-                    i -> {
-                        String art = i.path("art").asText("");
-                        String artZusatz = i.path("artZusatz").asText("");
-                        return !artZusatz.isEmpty() ? art + " (" + artZusatz + ")" : art;
-                    }, hasChanges);
-                
-                String faktorHtml = buildEinkommenFieldHtml(rightItem, leftItem,
-                    i -> {
-                        double faktor = i.path("faktor").asDouble(12);
-                        return faktor == 12 ? "monatlich" : 
-                               faktor == 1 ? "jährlich" : 
-                               String.format("%.0fx/Jahr", faktor);
-                    }, hasChanges);
-                
-                String betragHtml = buildEinkommenFieldHtml(rightItem, leftItem,
-                    i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
-                
-                String zuschlagHtml = buildEinkommenFieldHtml(rightItem, leftItem,
-                    i -> {
-                        double zuschlag = i.path("zuschlag").asDouble(0.0);
-                        return zuschlag > 0 ? String.format("%.2f €", zuschlag) : "";
-                    }, hasChanges);
-                
-                String abschlagHtml = buildEinkommenFieldHtml(rightItem, leftItem,
-                    i -> {
-                        double abschlag = i.path("abschlag").asDouble(0.0);
-                        return abschlag > 0 ? String.format("%.2f €", abschlag) : "";
-                    }, hasChanges);
-                
-                String werbungskostenHtml = buildEinkommenFieldHtml(rightItem, leftItem,
-                    i -> {
-                        double werbungskosten = i.path("werbungskosten").asDouble(0.0);
-                        return werbungskosten > 0 ? String.format("%.2f €", werbungskosten) : "";
-                    }, hasChanges);
-                
-                result.add(new EinkommenZeile(
-                    symbol, symbolClass, rowClass,
-                    artHtml, faktorHtml, betragHtml,
-                    zuschlagHtml, abschlagHtml, werbungskostenHtml
-                ));
-            }
-        }
-        
-        return result;
-    }
-    
-    private static boolean detectEinkommenChanges(JsonNode rightItem, JsonNode leftItem) {
-        return !Objects.equals(rightItem.path("art").asText(""), leftItem.path("art").asText("")) ||
-               !Objects.equals(rightItem.path("artZusatz").asText(""), leftItem.path("artZusatz").asText("")) ||
-               rightItem.path("faktor").asDouble(12) != leftItem.path("faktor").asDouble(12) ||
-               rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0) ||
-               rightItem.path("zuschlag").asDouble(0.0) != leftItem.path("zuschlag").asDouble(0.0) ||
-               rightItem.path("abschlag").asDouble(0.0) != leftItem.path("abschlag").asDouble(0.0) ||
-               rightItem.path("werbungskosten").asDouble(0.0) != leftItem.path("werbungskosten").asDouble(0.0);
-    }
-    
-    private static String buildEinkommenFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
-        if (!hasChanges || rightItem == null) {
-            JsonNode primary = rightItem != null ? rightItem : leftItem;
-            return primary != null ? escape(extractor.apply(primary)) : "";
-        }
-        
-        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
-        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
-        
-        if (!Objects.equals(rightVal, leftVal)) {
-            return cellHtml(rightVal, leftVal, true);
-        } else {
-            return escape(rightVal);
-        }
-    }
-    
-    private static List<FreibetragZeile> buildFreibetragZeilen(JsonNode rightPerson, JsonNode leftPerson) {
-        // Extract freibetraege arrays from both persons
-        ArrayNode rightFreibetraege = rightPerson != null && rightPerson.has("freibetraege") && rightPerson.get("freibetraege").isArray()
-                ? (ArrayNode) rightPerson.get("freibetraege")
-                : new ObjectMapper().createArrayNode();
-        ArrayNode leftFreibetraege = leftPerson != null && leftPerson.has("freibetraege") && leftPerson.get("freibetraege").isArray()
-                ? (ArrayNode) leftPerson.get("freibetraege")
-                : new ObjectMapper().createArrayNode();
-        
-        // UNION über freibetraege[].id
-        Map<String, Pair<JsonNode, JsonNode>> freibetragPairs = unionByKey(
-                rightFreibetraege, leftFreibetraege, n -> n.path("id").asText()
-        );
-        
-        List<FreibetragZeile> result = new ArrayList<>();
-        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : freibetragPairs.entrySet()) {
-            JsonNode rightItem = entry.getValue().right;
-            JsonNode leftItem = entry.getValue().left;
-            
-            // Determine status and changes
-            String symbol = "";
-            String symbolClass = "";
-            String rowClass = "";
-            boolean hasChanges = false;
-            
-            if (rightItem != null && leftItem != null) {
-                hasChanges = detectFreibetragChanges(rightItem, leftItem);
-                if (hasChanges) {
-                    symbol = symFor("CHANGED");
-                    symbolClass = symClassFor("CHANGED");
-                }
-            } else if (rightItem != null && leftItem == null) {
-                symbol = symFor("ADDED");
-                symbolClass = symClassFor("ADDED");
-                rowClass = rowClassFor("ADDED");
-            } else if (rightItem == null && leftItem != null) {
-                symbol = symFor("REMOVED");
-                symbolClass = symClassFor("REMOVED");
-                rowClass = rowClassFor("REMOVED");
-            }
-            
-            // Use primary data (right first, then left)
-            JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
-            
-            if (primaryItem != null) {
-                String artHtml = buildFreibetragFieldHtml(rightItem, leftItem,
-                    i -> i.path("art").asText(""), hasChanges);
-                
-                String frequenzHtml = buildFreibetragFieldHtml(rightItem, leftItem,
-                    i -> {
-                        double faktor = i.path("faktor").asDouble(12);
-                        return faktor == 12 ? "monatlich" : 
-                               faktor == 1 ? "jährlich" : 
-                               String.format("%.0fx/Jahr", faktor);
-                    }, hasChanges);
-                
-                String betragHtml = buildFreibetragFieldHtml(rightItem, leftItem,
-                    i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
-                
-                String fristHtml = buildFreibetragFieldHtml(rightItem, leftItem,
-                    i -> {
-                        String fristDatum = i.path("fristDatum").asText("");
-                        return !fristDatum.isEmpty() ? FormatUtil.date(fristDatum) : "";
-                    }, hasChanges);
-                
-                String typ = primaryItem.path("typ").asText("F"); // Default to "F" if not specified
-                
-                result.add(new FreibetragZeile(
-                    symbol, symbolClass, rowClass,
-                    typ, artHtml, frequenzHtml, betragHtml, fristHtml
-                ));
-            }
-        }
-        
-        return result;
-    }
-    
-    private static boolean detectFreibetragChanges(JsonNode rightItem, JsonNode leftItem) {
-        return !Objects.equals(rightItem.path("art").asText(""), leftItem.path("art").asText("")) ||
-               rightItem.path("faktor").asDouble(12) != leftItem.path("faktor").asDouble(12) ||
-               rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0) ||
-               !Objects.equals(rightItem.path("fristDatum").asText(""), leftItem.path("fristDatum").asText(""));
-    }
-    
-    private static String buildFreibetragFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
-        if (!hasChanges || rightItem == null) {
-            JsonNode primary = rightItem != null ? rightItem : leftItem;
-            return primary != null ? escape(extractor.apply(primary)) : "";
-        }
-        
-        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
-        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
-        
-        if (!Objects.equals(rightVal, leftVal)) {
-            return cellHtml(rightVal, leftVal, true);
-        } else {
-            return escape(rightVal);
-        }
-    }
-    
-    private static List<KitaZeile> buildKitaZeilen(JsonNode rightPerson, JsonNode leftPerson, Map<String, String> personNameByIdRight) {
-        // Extract kinderbetreuung arrays from both persons
-        ArrayNode rightKinderbetreuung = rightPerson != null && rightPerson.has("kinderbetreuung") && rightPerson.get("kinderbetreuung").isArray()
-                ? (ArrayNode) rightPerson.get("kinderbetreuung")
-                : new ObjectMapper().createArrayNode();
-        ArrayNode leftKinderbetreuung = leftPerson != null && leftPerson.has("kinderbetreuung") && leftPerson.get("kinderbetreuung").isArray()
-                ? (ArrayNode) leftPerson.get("kinderbetreuung")
-                : new ObjectMapper().createArrayNode();
-        
-        // UNION über kinderbetreuung[].id
-        Map<String, Pair<JsonNode, JsonNode>> kitaPairs = unionByKey(
-                rightKinderbetreuung, leftKinderbetreuung, n -> n.path("id").asText()
-        );
-        
-        List<KitaZeile> result = new ArrayList<>();
-        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : kitaPairs.entrySet()) {
-            JsonNode rightItem = entry.getValue().right;
-            JsonNode leftItem = entry.getValue().left;
-            
-            // Determine status and changes
-            String symbol = "";
-            String symbolClass = "";
-            String rowClass = "";
-            boolean hasChanges = false;
-            
-            if (rightItem != null && leftItem != null) {
-                hasChanges = detectKitaChanges(rightItem, leftItem);
-                if (hasChanges) {
-                    symbol = symFor("CHANGED");
-                    symbolClass = symClassFor("CHANGED");
-                }
-            } else if (rightItem != null && leftItem == null) {
-                symbol = symFor("ADDED");
-                symbolClass = symClassFor("ADDED");
-                rowClass = rowClassFor("ADDED");
-            } else if (rightItem == null && leftItem != null) {
-                symbol = symFor("REMOVED");
-                symbolClass = symClassFor("REMOVED");
-                rowClass = rowClassFor("REMOVED");
-            }
-            
-            // Use primary data (right first, then left)
-            JsonNode primaryItem = rightItem != null ? rightItem : leftItem;
-            
-            if (primaryItem != null) {
-                String kindNameHtml = buildKitaFieldHtml(rightItem, leftItem,
-                    i -> {
-                        String kindRef = i.path("kindRef").asText("");
-                        return personNameByIdRight.getOrDefault(kindRef, "Kind ID: " + kindRef);
-                    }, hasChanges);
-                
-                String betragHtml = buildKitaFieldHtml(rightItem, leftItem,
-                    i -> String.format("%.2f €", i.path("betrag").asDouble(0.0)), hasChanges);
-                
-                result.add(new KitaZeile(
-                    symbol, symbolClass, rowClass,
-                    kindNameHtml, betragHtml
-                ));
-            }
-        }
-        
-        return result;
-    }
-    
-    private static boolean detectKitaChanges(JsonNode rightItem, JsonNode leftItem) {
-        return !Objects.equals(rightItem.path("kindRef").asText(""), leftItem.path("kindRef").asText("")) ||
-               rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0);
-    }
-    
-    private static String buildKitaFieldHtml(JsonNode rightItem, JsonNode leftItem, Function<JsonNode, String> extractor, boolean hasChanges) {
-        if (!hasChanges || rightItem == null) {
-            JsonNode primary = rightItem != null ? rightItem : leftItem;
-            return primary != null ? escape(extractor.apply(primary)) : "";
-        }
-        
-        String rightVal = rightItem != null ? extractor.apply(rightItem) : "";
-        String leftVal = leftItem != null ? extractor.apply(leftItem) : "";
-        
-        if (!Objects.equals(rightVal, leftVal)) {
-            return cellHtml(rightVal, leftVal, true);
-        } else {
-            return escape(rightVal);
-        }
-    }
-
-    // Hilfs-Bausteine für Status und Zellen:
-    private static String symFor(String status) {
-        return switch (status) {
-            case "ADDED" -> "+";
-            case "REMOVED" -> "−";
-            case "CHANGED" -> "!";
-            default -> "";
-        };
-    }
-    
-    private static String symClassFor(String status) {
-        return switch (status) {
-            case "ADDED" -> "plus";
-            case "REMOVED" -> "minus";
-            case "CHANGED" -> "warn";
-            default -> "";
-        };
-    }
-    
-    private static String rowClassFor(String status) {
-        return switch (status) {
-            case "ADDED" -> "diff-row-added";
-            case "REMOVED" -> "diff-row-removed";
-            default -> "";
-        };
-    }
-    
-    private static String cellHtml(String newVal, String oldVal, boolean changed) {
-        if (!changed) {
-            return escape(newVal);
-        }
-        
-        // Handle empty old values
-        String displayOldVal = (oldVal == null || oldVal.trim().isEmpty()) ? "leer" : oldVal;
-        
-        // Return with cell-changed class and proper old value display
-        return "<span class='cell-changed'>" + escape(newVal) + 
-               "<div class='old'>Alt: " + escape(displayOldVal) + "</div></span>";
-    }
-    
-    private static String escape(String s) {
-        return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">");
-    }
 }

+ 295 - 0
src/main/resources/static/example-alt.json

@@ -99,6 +99,301 @@
             ]
           }
         ]
+      },
+      {
+        "stammvorgang": {
+          "id": "stammvorgang-002",
+          "vorgangstyp": "Weiterleistungsantrag",
+          "vorgangsstatus": "in Bearbeitung"
+        },
+        "zeitraeume": [
+          {
+            "id": "zeitraum-002",
+            "beginn": "2025-01-01",
+            "ende": "2025-06-30",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": {
+                  "id": "person-001",
+                  "nachname": "Müller",
+                  "vorname": "Tobias",
+                  "geburtsdatum": "1990-05-15",
+                  "geschlecht": "männlich",
+                  "familienstand": "verheiratet",
+                  "zahlungsempfaenger": true
+                },
+                "einkommen": [
+                  {
+                    "id": "einkommen-003",
+                    "art": "Erwerbstätigkeit",
+                    "artZusatz": "Vollzeit",
+                    "faktor": 12,
+                    "betrag": 2950.00,
+                    "werbungskosten": 120.00,
+                    "zuschlag": 100.00
+                  }
+                ],
+                "freibetraege": [
+                  {
+                    "id": "freibetrag-002",
+                    "typ": "F",
+                    "art": "Erwerbstätigenfreibetrag",
+                    "faktor": 12,
+                    "betrag": 320.00
+                  },
+                  {
+                    "id": "pauschale-002",
+                    "typ": "P",
+                    "art": "Versicherungspauschale",
+                    "faktor": 12,
+                    "betrag": 35.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Partner",
+                "person": {
+                  "id": "person-002",
+                  "nachname": "Müller",
+                  "vorname": "Anna",
+                  "geburtsdatum": "1992-08-20",
+                  "geschlecht": "weiblich",
+                  "familienstand": "verheiratet"
+                },
+                "einkommen": [
+                  {
+                    "id": "einkommen-004",
+                    "art": "Teilzeit",
+                    "faktor": 12,
+                    "betrag": 800.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "person-003",
+                  "nachname": "Müller",
+                  "vorname": "Max",
+                  "geburtsdatum": "2018-03-10",
+                  "geschlecht": "männlich"
+                },
+                "kinderbetreuung": [
+                  {
+                    "id": "kita-002",
+                    "kindRef": "person-003",
+                    "betrag": 180.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "person-004",
+                  "nachname": "Müller",
+                  "vorname": "Lisa",
+                  "geburtsdatum": "2021-11-05",
+                  "geschlecht": "weiblich"
+                }
+              }
+            ]
+          },
+          {
+            "id": "zeitraum-003",
+            "beginn": "2025-07-01",
+            "ende": "2025-12-31",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": {
+                  "id": "person-001",
+                  "nachname": "Müller",
+                  "vorname": "Tobias",
+                  "geburtsdatum": "1990-05-15",
+                  "geschlecht": "männlich",
+                  "familienstand": "verheiratet",
+                  "zahlungsempfaenger": true
+                },
+                "einkommen": [
+                  {
+                    "id": "einkommen-005",
+                    "art": "Erwerbstätigkeit",
+                    "artZusatz": "Vollzeit",
+                    "faktor": 12,
+                    "betrag": 3100.00,
+                    "werbungskosten": 120.00,
+                    "zuschlag": 150.00
+                  }
+                ],
+                "freibetraege": [
+                  {
+                    "id": "freibetrag-003",
+                    "typ": "F",
+                    "art": "Erwerbstätigenfreibetrag",
+                    "faktor": 12,
+                    "betrag": 350.00
+                  },
+                  {
+                    "id": "pauschale-003",
+                    "typ": "P",
+                    "art": "Versicherungspauschale",
+                    "faktor": 12,
+                    "betrag": 40.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Partner",
+                "person": {
+                  "id": "person-002",
+                  "nachname": "Müller",
+                  "vorname": "Anna",
+                  "geburtsdatum": "1992-08-20",
+                  "geschlecht": "weiblich",
+                  "familienstand": "verheiratet"
+                },
+                "einkommen": [
+                  {
+                    "id": "einkommen-006",
+                    "art": "Teilzeit",
+                    "faktor": 12,
+                    "betrag": 1200.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "person-003",
+                  "nachname": "Müller",
+                  "vorname": "Max",
+                  "geburtsdatum": "2018-03-10",
+                  "geschlecht": "männlich"
+                },
+                "kinderbetreuung": [
+                  {
+                    "id": "kita-003",
+                    "kindRef": "person-003",
+                    "betrag": 200.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "person-004",
+                  "nachname": "Müller",
+                  "vorname": "Lisa",
+                  "geburtsdatum": "2021-11-05",
+                  "geschlecht": "weiblich"
+                },
+                "kinderbetreuung": [
+                  {
+                    "id": "kita-004",
+                    "kindRef": "person-004",
+                    "betrag": 120.00
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "stammvorgang": {
+          "id": "stammvorgang-003",
+          "vorgangstyp": "Änderungsantrag",
+          "vorgangsstatus": "eingereicht"
+        },
+        "zeitraeume": [
+          {
+            "id": "zeitraum-004",
+            "beginn": "2026-01-01",
+            "ende": "2026-12-31",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": {
+                  "id": "person-001",
+                  "nachname": "Müller",
+                  "vorname": "Tobias",
+                  "geburtsdatum": "1990-05-15",
+                  "geschlecht": "männlich",
+                  "familienstand": "verheiratet",
+                  "zahlungsempfaenger": true
+                },
+                "einkommen": [
+                  {
+                    "id": "einkommen-007",
+                    "art": "Erwerbstätigkeit",
+                    "artZusatz": "Vollzeit",
+                    "faktor": 12,
+                    "betrag": 3200.00,
+                    "werbungskosten": 130.00
+                  }
+                ],
+                "freibetraege": [
+                  {
+                    "id": "freibetrag-004",
+                    "typ": "F",
+                    "art": "Erwerbstätigenfreibetrag",
+                    "faktor": 12,
+                    "betrag": 360.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Partner",
+                "person": {
+                  "id": "person-002",
+                  "nachname": "Müller",
+                  "vorname": "Anna",
+                  "geburtsdatum": "1992-08-20",
+                  "geschlecht": "weiblich",
+                  "familienstand": "verheiratet"
+                },
+                "einkommen": [
+                  {
+                    "id": "einkommen-008",
+                    "art": "Vollzeit",
+                    "faktor": 12,
+                    "betrag": 2400.00
+                  }
+                ],
+                "freibetraege": [
+                  {
+                    "id": "freibetrag-005",
+                    "typ": "F",
+                    "art": "Erwerbstätigenfreibetrag",
+                    "faktor": 12,
+                    "betrag": 240.00
+                  }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "person-003",
+                  "nachname": "Müller",
+                  "vorname": "Max",
+                  "geburtsdatum": "2018-03-10",
+                  "geschlecht": "männlich"
+                }
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "person-004",
+                  "nachname": "Müller",
+                  "vorname": "Lisa",
+                  "geburtsdatum": "2021-11-05",
+                  "geschlecht": "weiblich"
+                }
+              }
+            ]
+          }
+        ]
       }
     ]
   }

+ 0 - 295
src/main/resources/static/example-neu.json

@@ -99,301 +99,6 @@
             ]
           }
         ]
-      },
-      {
-        "stammvorgang": {
-          "id": "stammvorgang-002",
-          "vorgangstyp": "Weiterleistungsantrag",
-          "vorgangsstatus": "in Bearbeitung"
-        },
-        "zeitraeume": [
-          {
-            "id": "zeitraum-002",
-            "beginn": "2025-01-01",
-            "ende": "2025-06-30",
-            "personen": [
-              {
-                "rolleImHaushalt": "Antragsteller",
-                "person": {
-                  "id": "person-001",
-                  "nachname": "Müller",
-                  "vorname": "Tobias",
-                  "geburtsdatum": "1990-05-15",
-                  "geschlecht": "männlich",
-                  "familienstand": "verheiratet",
-                  "zahlungsempfaenger": true
-                },
-                "einkommen": [
-                  {
-                    "id": "einkommen-003",
-                    "art": "Erwerbstätigkeit",
-                    "artZusatz": "Vollzeit",
-                    "faktor": 12,
-                    "betrag": 2950.00,
-                    "werbungskosten": 120.00,
-                    "zuschlag": 100.00
-                  }
-                ],
-                "freibetraege": [
-                  {
-                    "id": "freibetrag-002",
-                    "typ": "F",
-                    "art": "Erwerbstätigenfreibetrag",
-                    "faktor": 12,
-                    "betrag": 320.00
-                  },
-                  {
-                    "id": "pauschale-002",
-                    "typ": "P",
-                    "art": "Versicherungspauschale",
-                    "faktor": 12,
-                    "betrag": 35.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Partner",
-                "person": {
-                  "id": "person-002",
-                  "nachname": "Müller",
-                  "vorname": "Anna",
-                  "geburtsdatum": "1992-08-20",
-                  "geschlecht": "weiblich",
-                  "familienstand": "verheiratet"
-                },
-                "einkommen": [
-                  {
-                    "id": "einkommen-004",
-                    "art": "Teilzeit",
-                    "faktor": 12,
-                    "betrag": 800.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Kind",
-                "person": {
-                  "id": "person-003",
-                  "nachname": "Müller",
-                  "vorname": "Max",
-                  "geburtsdatum": "2018-03-10",
-                  "geschlecht": "männlich"
-                },
-                "kinderbetreuung": [
-                  {
-                    "id": "kita-002",
-                    "kindRef": "person-003",
-                    "betrag": 180.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Kind",
-                "person": {
-                  "id": "person-004",
-                  "nachname": "Müller",
-                  "vorname": "Lisa",
-                  "geburtsdatum": "2021-11-05",
-                  "geschlecht": "weiblich"
-                }
-              }
-            ]
-          },
-          {
-            "id": "zeitraum-003",
-            "beginn": "2025-07-01",
-            "ende": "2025-12-31",
-            "personen": [
-              {
-                "rolleImHaushalt": "Antragsteller",
-                "person": {
-                  "id": "person-001",
-                  "nachname": "Müller",
-                  "vorname": "Tobias",
-                  "geburtsdatum": "1990-05-15",
-                  "geschlecht": "männlich",
-                  "familienstand": "verheiratet",
-                  "zahlungsempfaenger": true
-                },
-                "einkommen": [
-                  {
-                    "id": "einkommen-005",
-                    "art": "Erwerbstätigkeit",
-                    "artZusatz": "Vollzeit",
-                    "faktor": 12,
-                    "betrag": 3100.00,
-                    "werbungskosten": 120.00,
-                    "zuschlag": 150.00
-                  }
-                ],
-                "freibetraege": [
-                  {
-                    "id": "freibetrag-003",
-                    "typ": "F",
-                    "art": "Erwerbstätigenfreibetrag",
-                    "faktor": 12,
-                    "betrag": 350.00
-                  },
-                  {
-                    "id": "pauschale-003",
-                    "typ": "P",
-                    "art": "Versicherungspauschale",
-                    "faktor": 12,
-                    "betrag": 40.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Partner",
-                "person": {
-                  "id": "person-002",
-                  "nachname": "Müller",
-                  "vorname": "Anna",
-                  "geburtsdatum": "1992-08-20",
-                  "geschlecht": "weiblich",
-                  "familienstand": "verheiratet"
-                },
-                "einkommen": [
-                  {
-                    "id": "einkommen-006",
-                    "art": "Teilzeit",
-                    "faktor": 12,
-                    "betrag": 1200.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Kind",
-                "person": {
-                  "id": "person-003",
-                  "nachname": "Müller",
-                  "vorname": "Max",
-                  "geburtsdatum": "2018-03-10",
-                  "geschlecht": "männlich"
-                },
-                "kinderbetreuung": [
-                  {
-                    "id": "kita-003",
-                    "kindRef": "person-003",
-                    "betrag": 200.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Kind",
-                "person": {
-                  "id": "person-004",
-                  "nachname": "Müller",
-                  "vorname": "Lisa",
-                  "geburtsdatum": "2021-11-05",
-                  "geschlecht": "weiblich"
-                },
-                "kinderbetreuung": [
-                  {
-                    "id": "kita-004",
-                    "kindRef": "person-004",
-                    "betrag": 120.00
-                  }
-                ]
-              }
-            ]
-          }
-        ]
-      },
-      {
-        "stammvorgang": {
-          "id": "stammvorgang-003",
-          "vorgangstyp": "Änderungsantrag",
-          "vorgangsstatus": "eingereicht"
-        },
-        "zeitraeume": [
-          {
-            "id": "zeitraum-004",
-            "beginn": "2026-01-01",
-            "ende": "2026-12-31",
-            "personen": [
-              {
-                "rolleImHaushalt": "Antragsteller",
-                "person": {
-                  "id": "person-001",
-                  "nachname": "Müller",
-                  "vorname": "Tobias",
-                  "geburtsdatum": "1990-05-15",
-                  "geschlecht": "männlich",
-                  "familienstand": "verheiratet",
-                  "zahlungsempfaenger": true
-                },
-                "einkommen": [
-                  {
-                    "id": "einkommen-007",
-                    "art": "Erwerbstätigkeit",
-                    "artZusatz": "Vollzeit",
-                    "faktor": 12,
-                    "betrag": 3200.00,
-                    "werbungskosten": 130.00
-                  }
-                ],
-                "freibetraege": [
-                  {
-                    "id": "freibetrag-004",
-                    "typ": "F",
-                    "art": "Erwerbstätigenfreibetrag",
-                    "faktor": 12,
-                    "betrag": 360.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Partner",
-                "person": {
-                  "id": "person-002",
-                  "nachname": "Müller",
-                  "vorname": "Anna",
-                  "geburtsdatum": "1992-08-20",
-                  "geschlecht": "weiblich",
-                  "familienstand": "verheiratet"
-                },
-                "einkommen": [
-                  {
-                    "id": "einkommen-008",
-                    "art": "Vollzeit",
-                    "faktor": 12,
-                    "betrag": 2400.00
-                  }
-                ],
-                "freibetraege": [
-                  {
-                    "id": "freibetrag-005",
-                    "typ": "F",
-                    "art": "Erwerbstätigenfreibetrag",
-                    "faktor": 12,
-                    "betrag": 240.00
-                  }
-                ]
-              },
-              {
-                "rolleImHaushalt": "Kind",
-                "person": {
-                  "id": "person-003",
-                  "nachname": "Müller",
-                  "vorname": "Max",
-                  "geburtsdatum": "2018-03-10",
-                  "geschlecht": "männlich"
-                }
-              },
-              {
-                "rolleImHaushalt": "Kind",
-                "person": {
-                  "id": "person-004",
-                  "nachname": "Müller",
-                  "vorname": "Lisa",
-                  "geburtsdatum": "2021-11-05",
-                  "geschlecht": "weiblich"
-                }
-              }
-            ]
-          }
-        ]
       }
     ]
   }

+ 33 - 10
src/main/resources/templates/vergleich.html

@@ -53,23 +53,46 @@
         </span>
       </div>
       
-      <div class="zeitraum-controls">
-        <form method="post" action="/vergleich" class="zeitraum-form" role="form" aria-label="Zeitraum wechseln">
+      <div class="vergleich-controls">
+        <!-- Vorgangsauswahl (links/alt) -->
+        <form method="post" action="/vergleich" class="vorgang-form" role="form" aria-label="Vorgangsauswahl">
+          <input type="hidden" name="schemaName" th:value="${schemaName}"/>
+          <input type="hidden" name="validate" th:value="${validate}"/>
+          <input type="hidden" name="leftJson" th:value="${leftJson}"/>
+          <input type="hidden" name="rightJson" th:value="${rightJson}"/>
+          <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
+          <input type="hidden" name="personId" th:value="${personId}"/>
+          <input type="hidden" name="kunde" th:value="${activeKunde}"/>
+          
+          <div class="control-group">
+            <label for="leftVorgangId">Vergleichsvorgang:</label>
+            <select name="leftVorgangId" id="leftVorgangId" class="vorgang-select" onchange="this.form.submit()">
+              <option th:each="v : ${leftCandidates}" th:value="${v.id}"
+                      th:selected="${v.id == leftVorgangId}"
+                      th:text="${v.vorgangstyp + (!#strings.isEmpty(v.vorgangstyp) ? ' (' + v.id + ')' : v.id)}"></option>
+            </select>
+          </div>
+        </form>
+        
+        <!-- Zeitraumauswahl (links/alt) -->
+        <form method="post" action="/vergleich" class="zeitraum-form" role="form" aria-label="Zeitraumauswahl">
           <input type="hidden" name="schemaName" th:value="${schemaName}"/>
           <input type="hidden" name="validate" th:value="${validate}"/>
           <input type="hidden" name="leftJson" th:value="${leftJson}"/>
           <input type="hidden" name="rightJson" th:value="${rightJson}"/>
           <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
-          <input type="hidden" name="leftZeitraumId" th:value="${leftZeitraumId}"/>
+          <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
+          <input type="hidden" name="personId" th:value="${personId}"/>
           <input type="hidden" name="kunde" th:value="${activeKunde}"/>
           
-          <label for="rightZeitraumId">Zeitraum:</label>
-          <select name="rightZeitraumId" id="rightZeitraumId" class="zeitraum-select" aria-describedby="zeitraum-hint">
-            <option th:each="z : ${zeitraeumeRight}" th:value="${z.id}"
-                    th:selected="${z.id == rightZeitraumId}"
-                    th:text="${z.beginn} + ' - ' + ${z.ende}"></option>
-          </select>
-          <button type="submit" class="wechseln-btn" aria-label="Zeitraum wechseln">▼</button>
+          <div class="control-group">
+            <label for="leftZeitraumId">Zeitraum:</label>
+            <select name="leftZeitraumId" id="leftZeitraumId" class="zeitraum-select" onchange="this.form.submit()">
+              <option th:each="z : ${leftZeitraeumeOfSelected}" th:value="${z.id}"
+                      th:selected="${z.id == leftZeitraumId}"
+                      th:text="${z.beginn} + ' - ' + ${z.ende}"></option>
+            </select>
+          </div>
         </form>
       </div>
     </div>