Selaa lähdekoodia

Meite ergänzt und CSS aufgeeräumt, Testdaten verbessert

Leonard Schulz 1 vuosi sitten
vanhempi
sitoutus
0669060e26
24 muutettua tiedostoa jossa 2726 lisäystä ja 740 poistoa
  1. 62 0
      src/main/java/com/tsystems/inwo/vgl/service/DataComparisonService.java
  2. 64 0
      src/main/java/com/tsystems/inwo/vgl/service/HtmlRenderingService.java
  3. 434 0
      src/main/java/com/tsystems/inwo/vgl/service/VergleichDataExtractor.java
  4. 52 0
      src/main/java/com/tsystems/inwo/vgl/view/MieteZeile.java
  5. 15 0
      src/main/java/com/tsystems/inwo/vgl/view/MitbewohnerZeile.java
  6. 18 0
      src/main/java/com/tsystems/inwo/vgl/view/UntermieterZeile.java
  7. 24 0
      src/main/java/com/tsystems/inwo/vgl/view/WohnungZeile.java
  8. 1 0
      src/main/java/com/tsystems/inwo/vgl/web/HhmEinkommenController.java
  9. 15 0
      src/main/java/com/tsystems/inwo/vgl/web/MieteController.java
  10. 310 0
      src/main/resources/static/css/global.css
  11. 145 0
      src/main/resources/static/css/header.css
  12. 178 0
      src/main/resources/static/css/hhm-einkommen.css
  13. 134 0
      src/main/resources/static/css/legend.css
  14. 136 0
      src/main/resources/static/css/miete.css
  15. 142 6
      src/main/resources/static/example-miete-alt.json
  16. 205 6
      src/main/resources/static/example-miete-neu.json
  17. 5 5
      src/main/resources/static/js/ajax-handlers.js
  18. 540 409
      src/main/resources/static/styles.css
  19. 1 1
      src/main/resources/templates/fragments/header.html
  20. 1 1
      src/main/resources/templates/fragments/legend.html
  21. 5 2
      src/main/resources/templates/hhm-einkommen.html
  22. 237 30
      src/main/resources/templates/miete.html
  23. 2 2
      src/main/resources/templates/test.html
  24. 0 278
      src/main/resources/templates/vergleich.html

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

@@ -91,6 +91,18 @@ public class DataComparisonService {
                rightItem.path("betrag").asDouble(0.0) != leftItem.path("betrag").asDouble(0.0);
     }
 
+    public boolean detectAusstattungChanges(JsonNode rightAusstattung, JsonNode leftAusstattung) {
+        return !Objects.equals(rightAusstattung.path("mietverhaeltnis").asText(""), leftAusstattung.path("mietverhaeltnis").asText("")) ||
+               rightAusstattung.path("flaeche").asDouble(0.0) != leftAusstattung.path("flaeche").asDouble(0.0) ||
+               !Objects.equals(rightAusstattung.path("bad").asText(""), leftAusstattung.path("bad").asText("")) ||
+               !Objects.equals(rightAusstattung.path("nameEig").asText(""), leftAusstattung.path("nameEig").asText("")) ||
+               !Objects.equals(rightAusstattung.path("telefonEig").asText(""), leftAusstattung.path("telefonEig").asText("")) ||
+               rightAusstattung.path("sozBauJn").asBoolean(false) != leftAusstattung.path("sozBauJn").asBoolean(false) ||
+               !Objects.equals(rightAusstattung.path("fertigDat").asText(""), leftAusstattung.path("fertigDat").asText("")) ||
+               !Objects.equals(rightAusstattung.path("heizung").asText(""), leftAusstattung.path("heizung").asText("")) ||
+               !Objects.equals(rightAusstattung.path("adresseEig").asText(""), leftAusstattung.path("adresseEig").asText(""));
+    }
+
     public String getSymbolFor(String status) {
         return switch (status) {
             case "ADDED" -> "+";
@@ -116,4 +128,54 @@ public class DataComparisonService {
             default -> "";
         };
     }
+
+    public boolean detectUntermieterChanges(JsonNode rightItem, JsonNode leftItem) {
+        return !Objects.equals(rightItem.path("name").asText(""), leftItem.path("name").asText("")) ||
+               !Objects.equals(rightItem.path("vorname").asText(""), leftItem.path("vorname").asText("")) ||
+               rightItem.path("miete").asDouble(0.0) != leftItem.path("miete").asDouble(0.0) ||
+               rightItem.path("flaeche").asDouble(0.0) != leftItem.path("flaeche").asDouble(0.0) ||
+               rightItem.path("sonst").asDouble(0.0) != leftItem.path("sonst").asDouble(0.0) ||
+               rightItem.path("heizung").asDouble(0.0) != leftItem.path("heizung").asDouble(0.0) ||
+               !Objects.equals(rightItem.path("heizungP").asText(""), leftItem.path("heizungP").asText("")) ||
+               rightItem.path("warmwasser").asDouble(0.0) != leftItem.path("warmwasser").asDouble(0.0) ||
+               !Objects.equals(rightItem.path("warmwasserP").asText(""), leftItem.path("warmwasserP").asText("")) ||
+               rightItem.path("moeblierung").asDouble(0.0) != leftItem.path("moeblierung").asDouble(0.0) ||
+               !Objects.equals(rightItem.path("moeblierungP").asText(""), leftItem.path("moeblierungP").asText(""));
+    }
+
+    public boolean detectMitbewohnerChanges(JsonNode rightItem, JsonNode leftItem) {
+        return !Objects.equals(rightItem.path("name").asText(""), leftItem.path("name").asText("")) ||
+               !Objects.equals(rightItem.path("vorname").asText(""), leftItem.path("vorname").asText("")) ||
+               rightItem.path("entgelt").asDouble(0.0) != leftItem.path("entgelt").asDouble(0.0) ||
+               rightItem.path("sonst").asDouble(0.0) != leftItem.path("sonst").asDouble(0.0) ||
+               rightItem.path("heizung").asDouble(0.0) != leftItem.path("heizung").asDouble(0.0) ||
+               !Objects.equals(rightItem.path("heizungP").asText(""), leftItem.path("heizungP").asText("")) ||
+               rightItem.path("warmwasser").asDouble(0.0) != leftItem.path("warmwasser").asDouble(0.0) ||
+               !Objects.equals(rightItem.path("warmwasserP").asText(""), leftItem.path("warmwasserP").asText(""));
+    }
+
+    public boolean detectMieteChanges(JsonNode rightMiete, JsonNode leftMiete) {
+        return rightMiete.path("miete").asDouble(0.0) != leftMiete.path("miete").asDouble(0.0) ||
+               rightMiete.path("mieteAlt").asDouble(0.0) != leftMiete.path("mieteAlt").asDouble(0.0) ||
+               rightMiete.path("heizung").asDouble(0.0) != leftMiete.path("heizung").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("heizungP").asText(""), leftMiete.path("heizungP").asText("")) ||
+               rightMiete.path("warmwasser").asDouble(0.0) != leftMiete.path("warmwasser").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("warmwasserP").asText(""), leftMiete.path("warmwasserP").asText("")) ||
+               rightMiete.path("moeblierung").asDouble(0.0) != leftMiete.path("moeblierung").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("moeblierungP").asText(""), leftMiete.path("moeblierungP").asText("")) ||
+               rightMiete.path("sonstZuschlag").asDouble(0.0) != leftMiete.path("sonstZuschlag").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("sonstZuschlagBeschr").asText(""), leftMiete.path("sonstZuschlagBeschr").asText("")) ||
+               rightMiete.path("beitragDritter").asDouble(0.0) != leftMiete.path("beitragDritter").asDouble(0.0) ||
+               rightMiete.path("energie").asDouble(0.0) != leftMiete.path("energie").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("energieP").asText(""), leftMiete.path("energieP").asText("")) ||
+               rightMiete.path("stellplatz").asDouble(0.0) != leftMiete.path("stellplatz").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("stellplatzP").asText(""), leftMiete.path("stellplatzP").asText("")) ||
+               rightMiete.path("umietZuschlag").asDouble(0.0) != leftMiete.path("umietZuschlag").asDouble(0.0) ||
+               rightMiete.path("gewerbeZuschlag").asDouble(0.0) != leftMiete.path("gewerbeZuschlag").asDouble(0.0) ||
+               !Objects.equals(rightMiete.path("gewerbeZuschlagP").asText(""), leftMiete.path("gewerbeZuschlagP").asText("")) ||
+               rightMiete.path("gewerbeFlaeche").asDouble(0.0) != leftMiete.path("gewerbeFlaeche").asDouble(0.0) ||
+               rightMiete.path("jhrlFernwarmw").asDouble(0.0) != leftMiete.path("jhrlFernwarmw").asDouble(0.0) ||
+               rightMiete.path("jhrlFernwaerme").asDouble(0.0) != leftMiete.path("jhrlFernwaerme").asDouble(0.0) ||
+               rightMiete.path("jhrlFernWUndW").asDouble(0.0) != leftMiete.path("jhrlFernWUndW").asDouble(0.0);
+    }
 }

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

@@ -89,6 +89,22 @@ public class HtmlRenderingService {
         }
     }
 
+    public String buildAusstattungFieldHtml(JsonNode rightAusstattung, JsonNode leftAusstattung, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightAusstattung == null) {
+            JsonNode primary = rightAusstattung != null ? rightAusstattung : leftAusstattung;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightAusstattung != null ? extractor.apply(rightAusstattung) : "";
+        String leftVal = leftAusstattung != null ? extractor.apply(leftAusstattung) : "";
+        
+        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);
@@ -108,4 +124,52 @@ public class HtmlRenderingService {
                 .replace("<", "<")
                 .replace(">", ">");
     }
+
+    public String buildUntermieterFieldHtml(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 buildMitbewohnerFieldHtml(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 buildMieteFieldHtml(JsonNode rightMiete, JsonNode leftMiete, Function<JsonNode, String> extractor, boolean hasChanges) {
+        if (!hasChanges || rightMiete == null) {
+            JsonNode primary = rightMiete != null ? rightMiete : leftMiete;
+            return primary != null ? escape(extractor.apply(primary)) : "";
+        }
+        
+        String rightVal = rightMiete != null ? extractor.apply(rightMiete) : "";
+        String leftVal = leftMiete != null ? extractor.apply(leftMiete) : "";
+        
+        if (!Objects.equals(rightVal, leftVal)) {
+            return cellHtml(rightVal, leftVal, true);
+        } else {
+            return escape(rightVal);
+        }
+    }
 }

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

@@ -6,7 +6,11 @@ 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.MieteZeile;
+import com.tsystems.inwo.vgl.view.MitbewohnerZeile;
 import com.tsystems.inwo.vgl.view.MitgliedRow;
+import com.tsystems.inwo.vgl.view.UntermieterZeile;
+import com.tsystems.inwo.vgl.view.WohnungZeile;
 import com.tsystems.inwo.vgl.view.SharedRecords.Pair;
 import com.tsystems.inwo.vgl.util.FormatUtil;
 import org.springframework.stereotype.Service;
@@ -428,4 +432,434 @@ public class VergleichDataExtractor {
         
         return result;
     }
+
+    public WohnungZeile buildWohnungZeile(JsonNode rightZeitraum, JsonNode leftZeitraum) {
+        // Extract ausstattung from both zeitraums
+        JsonNode rightAusstattung = rightZeitraum != null ? rightZeitraum.path("ausstattung") : null;
+        JsonNode leftAusstattung = leftZeitraum != null ? leftZeitraum.path("ausstattung") : null;
+        
+        // Determine status and detect changes
+        String symbol = "";
+        String symbolClass = "";
+        String rowClass = "";
+        boolean hasChanges = false;
+        
+        if (rightAusstattung != null && !rightAusstattung.isMissingNode() && 
+            leftAusstattung != null && !leftAusstattung.isMissingNode()) {
+            // Both exist - check for changes
+            hasChanges = comparisonService.detectAusstattungChanges(rightAusstattung, leftAusstattung);
+            if (hasChanges) {
+                symbol = comparisonService.getSymbolFor("CHANGED");
+                symbolClass = comparisonService.getSymbolClassFor("CHANGED");
+            }
+        } else if (rightAusstattung != null && !rightAusstattung.isMissingNode() && 
+                   (leftAusstattung == null || leftAusstattung.isMissingNode())) {
+            symbol = comparisonService.getSymbolFor("ADDED");
+            symbolClass = comparisonService.getSymbolClassFor("ADDED");
+            rowClass = comparisonService.getRowClassFor("ADDED");
+        } else if ((rightAusstattung == null || rightAusstattung.isMissingNode()) && 
+                   leftAusstattung != null && !leftAusstattung.isMissingNode()) {
+            symbol = comparisonService.getSymbolFor("REMOVED");
+            symbolClass = comparisonService.getSymbolClassFor("REMOVED");
+            rowClass = comparisonService.getRowClassFor("REMOVED");
+        }
+        
+        // Build HTML fields with change detection
+        String mietverhaeltnisHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("mietverhaeltnis").asText(""), hasChanges);
+        
+        String flaecheHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> String.format("%.2f", a.path("flaeche").asDouble(0.0)), hasChanges);
+        
+        String badHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("bad").asText(""), hasChanges);
+        
+        String nameEigHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("nameEig").asText(""), hasChanges);
+        
+        String telefonEigHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("telefonEig").asText(""), hasChanges);
+        
+        String sozBauJnHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("sozBauJn").asBoolean(false) ? "☑" : "☐", hasChanges);
+        
+        String fertigDatHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> {
+                String fertigDat = a.path("fertigDat").asText("");
+                return !fertigDat.isEmpty() ? FormatUtil.date(fertigDat) : "";
+            }, hasChanges);
+        
+        String heizungHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("heizung").asText(""), hasChanges);
+        
+        String adresseEigHtml = htmlService.buildAusstattungFieldHtml(rightAusstattung, leftAusstattung,
+            a -> a.path("adresseEig").asText(""), hasChanges);
+        
+        return new WohnungZeile(
+            symbol, symbolClass, rowClass,
+            mietverhaeltnisHtml, flaecheHtml, badHtml, nameEigHtml,
+            telefonEigHtml, sozBauJnHtml, fertigDatHtml, heizungHtml, adresseEigHtml
+        );
+    }
+
+    public List<UntermieterZeile> buildUntermieterZeilen(JsonNode rightZeitraum, JsonNode leftZeitraum) {
+        // Extract miete and then untermieter arrays from both zeitraums
+        JsonNode rightMiete = rightZeitraum != null ? rightZeitraum.path("miete") : null;
+        JsonNode leftMiete = leftZeitraum != null ? leftZeitraum.path("miete") : null;
+        
+        ArrayNode rightUntermieter = rightMiete != null && rightMiete.has("untermieter") && rightMiete.get("untermieter").isArray()
+                ? (ArrayNode) rightMiete.get("untermieter")
+                : new ObjectMapper().createArrayNode();
+        ArrayNode leftUntermieter = leftMiete != null && leftMiete.has("untermieter") && leftMiete.get("untermieter").isArray()
+                ? (ArrayNode) leftMiete.get("untermieter")
+                : new ObjectMapper().createArrayNode();
+        
+        // UNION über untermieter[].lfdnr (da lfdnr der eindeutige Identifier ist)
+        Map<String, Pair<JsonNode, JsonNode>> untermieterPairs = comparisonService.unionByKey(
+                rightUntermieter, leftUntermieter, n -> String.valueOf(n.path("lfdnr").asInt())
+        );
+        
+        List<UntermieterZeile> result = new ArrayList<>();
+        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : untermieterPairs.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.detectUntermieterChanges(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 nameHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> i.path("name").asText(""), hasChanges);
+                
+                String vornameHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> i.path("vorname").asText(""), hasChanges);
+                
+                String mieteHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> String.format("%.2f €", i.path("miete").asDouble(0.0)), hasChanges);
+                
+                String flaecheHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> String.format("%.2f m²", i.path("flaeche").asDouble(0.0)), hasChanges);
+                
+                String sonstHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double sonst = i.path("sonst").asDouble(0.0);
+                        return sonst > 0 ? String.format("%.2f €", sonst) : "";
+                    }, hasChanges);
+                
+                String heizungHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double heizung = i.path("heizung").asDouble(0.0);
+                        return heizung > 0 ? String.format("%.2f €", heizung) : "";
+                    }, hasChanges);
+                
+                String heizungPHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> i.path("heizungP").asText(""), hasChanges);
+                
+                String warmwasserHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double warmwasser = i.path("warmwasser").asDouble(0.0);
+                        return warmwasser > 0 ? String.format("%.2f €", warmwasser) : "";
+                    }, hasChanges);
+                
+                String warmwasserPHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> i.path("warmwasserP").asText(""), hasChanges);
+                
+                String moeblierungHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double moeblierung = i.path("moeblierung").asDouble(0.0);
+                        return moeblierung > 0 ? String.format("%.2f €", moeblierung) : "";
+                    }, hasChanges);
+                
+                String moeblierungPHtml = htmlService.buildUntermieterFieldHtml(rightItem, leftItem,
+                    i -> i.path("moeblierungP").asText(""), hasChanges);
+                
+                result.add(new UntermieterZeile(
+                    symbol, symbolClass, rowClass,
+                    nameHtml, vornameHtml, mieteHtml, flaecheHtml, sonstHtml,
+                    heizungHtml, heizungPHtml, warmwasserHtml, warmwasserPHtml,
+                    moeblierungHtml, moeblierungPHtml
+                ));
+            }
+        }
+        
+        return result;
+    }
+
+    public List<MitbewohnerZeile> buildMitbewohnerZeilen(JsonNode rightZeitraum, JsonNode leftZeitraum) {
+        // Extract miete and then mitbewohner arrays from both zeitraums
+        JsonNode rightMiete = rightZeitraum != null ? rightZeitraum.path("miete") : null;
+        JsonNode leftMiete = leftZeitraum != null ? leftZeitraum.path("miete") : null;
+        
+        ArrayNode rightMitbewohner = rightMiete != null && rightMiete.has("mitbewohner") && rightMiete.get("mitbewohner").isArray()
+                ? (ArrayNode) rightMiete.get("mitbewohner")
+                : new ObjectMapper().createArrayNode();
+        ArrayNode leftMitbewohner = leftMiete != null && leftMiete.has("mitbewohner") && leftMiete.get("mitbewohner").isArray()
+                ? (ArrayNode) leftMiete.get("mitbewohner")
+                : new ObjectMapper().createArrayNode();
+        
+        // UNION über mitbewohner[].lfdnr (da lfdnr der eindeutige Identifier ist)
+        Map<String, Pair<JsonNode, JsonNode>> mitbewohnerPairs = comparisonService.unionByKey(
+                rightMitbewohner, leftMitbewohner, n -> String.valueOf(n.path("lfdnr").asInt())
+        );
+        
+        List<MitbewohnerZeile> result = new ArrayList<>();
+        for (Map.Entry<String, Pair<JsonNode, JsonNode>> entry : mitbewohnerPairs.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.detectMitbewohnerChanges(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 nameHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> i.path("name").asText(""), hasChanges);
+                
+                String vornameHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> i.path("vorname").asText(""), hasChanges);
+                
+                String entgeltHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> String.format("%.2f €", i.path("entgelt").asDouble(0.0)), hasChanges);
+                
+                String sonstHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double sonst = i.path("sonst").asDouble(0.0);
+                        return sonst > 0 ? String.format("%.2f €", sonst) : "";
+                    }, hasChanges);
+                
+                String heizungHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double heizung = i.path("heizung").asDouble(0.0);
+                        return heizung > 0 ? String.format("%.2f €", heizung) : "";
+                    }, hasChanges);
+                
+                String heizungPHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> i.path("heizungP").asText(""), hasChanges);
+                
+                String warmwasserHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> {
+                        double warmwasser = i.path("warmwasser").asDouble(0.0);
+                        return warmwasser > 0 ? String.format("%.2f €", warmwasser) : "";
+                    }, hasChanges);
+                
+                String warmwasserPHtml = htmlService.buildMitbewohnerFieldHtml(rightItem, leftItem,
+                    i -> i.path("warmwasserP").asText(""), hasChanges);
+                
+                result.add(new MitbewohnerZeile(
+                    symbol, symbolClass, rowClass,
+                    nameHtml, vornameHtml, entgeltHtml, sonstHtml,
+                    heizungHtml, heizungPHtml, warmwasserHtml, warmwasserPHtml
+                ));
+            }
+        }
+        
+        return result;
+    }
+
+    public MieteZeile buildMieteZeile(JsonNode rightZeitraum, JsonNode leftZeitraum) {
+        // Extract miete from both zeitraums
+        JsonNode rightMiete = rightZeitraum != null ? rightZeitraum.path("miete") : null;
+        JsonNode leftMiete = leftZeitraum != null ? leftZeitraum.path("miete") : null;
+        
+        // Determine status and detect changes
+        String symbol = "";
+        String symbolClass = "";
+        String rowClass = "";
+        boolean hasChanges = false;
+        
+        if (rightMiete != null && !rightMiete.isMissingNode() && 
+            leftMiete != null && !leftMiete.isMissingNode()) {
+            // Both exist - check for changes
+            hasChanges = comparisonService.detectMieteChanges(rightMiete, leftMiete);
+            if (hasChanges) {
+                symbol = comparisonService.getSymbolFor("CHANGED");
+                symbolClass = comparisonService.getSymbolClassFor("CHANGED");
+            }
+        } else if (rightMiete != null && !rightMiete.isMissingNode() && 
+                   (leftMiete == null || leftMiete.isMissingNode())) {
+            symbol = comparisonService.getSymbolFor("ADDED");
+            symbolClass = comparisonService.getSymbolClassFor("ADDED");
+            rowClass = comparisonService.getRowClassFor("ADDED");
+        } else if ((rightMiete == null || rightMiete.isMissingNode()) && 
+                   leftMiete != null && !leftMiete.isMissingNode()) {
+            symbol = comparisonService.getSymbolFor("REMOVED");
+            symbolClass = comparisonService.getSymbolClassFor("REMOVED");
+            rowClass = comparisonService.getRowClassFor("REMOVED");
+        }
+        
+        // Build HTML fields with change detection
+        // Linke Spalte - Allgemeine Daten
+        String gesamtmieteHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> String.format("%.2f €", m.path("miete").asDouble(0.0)), hasChanges);
+        
+        String zuschlägeHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double sonstZuschlag = m.path("sonstZuschlag").asDouble(0.0);
+                double moeblierung = m.path("moeblierung").asDouble(0.0);
+                double gewerbeZuschlag = m.path("gewerbeZuschlag").asDouble(0.0);
+                double umietZuschlag = m.path("umietZuschlag").asDouble(0.0);
+                double total = sonstZuschlag + moeblierung + gewerbeZuschlag + umietZuschlag;
+                return total > 0 ? String.format("%.2f €", total) : "";
+            }, hasChanges);
+        
+        String artZuschlagHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("sonstZuschlagBeschr").asText(""), hasChanges);
+        
+        String jhrlFernwaermeHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double jhrlFernwaerme = m.path("jhrlFernwaerme").asDouble(0.0);
+                return jhrlFernwaerme > 0 ? String.format("%.2f €", jhrlFernwaerme) : "";
+            }, hasChanges);
+        
+        String jhrlFernwarmwasserHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double jhrlFernwarmw = m.path("jhrlFernwarmw").asDouble(0.0);
+                return jhrlFernwarmw > 0 ? String.format("%.2f €", jhrlFernwarmw) : "";
+            }, hasChanges);
+        
+        String jhrlFernwUndWHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double jhrlFernWUndW = m.path("jhrlFernWUndW").asDouble(0.0);
+                return jhrlFernWUndW > 0 ? String.format("%.2f €", jhrlFernWUndW) : "";
+            }, hasChanges);
+        
+        // Mittlere Spalte - Abzüge
+        String heizungHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double heizung = m.path("heizung").asDouble(0.0);
+                return heizung > 0 ? String.format("%.2f €", heizung) : "";
+            }, hasChanges);
+        
+        String warmwasserHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double warmwasser = m.path("warmwasser").asDouble(0.0);
+                return warmwasser > 0 ? String.format("%.2f €", warmwasser) : "";
+            }, hasChanges);
+        
+        String kostenHhEnergieHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double energie = m.path("energie").asDouble(0.0);
+                return energie > 0 ? String.format("%.2f €", energie) : "";
+            }, hasChanges);
+        
+        String stellplatzHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double stellplatz = m.path("stellplatz").asDouble(0.0);
+                return stellplatz > 0 ? String.format("%.2f €", stellplatz) : "";
+            }, hasChanges);
+        
+        String moeblierungszuschlagHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double moeblierung = m.path("moeblierung").asDouble(0.0);
+                return moeblierung > 0 ? String.format("%.2f €", moeblierung) : "";
+            }, hasChanges);
+        
+        String gewerbezuschlagHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double gewerbeZuschlag = m.path("gewerbeZuschlag").asDouble(0.0);
+                return gewerbeZuschlag > 0 ? String.format("%.2f €", gewerbeZuschlag) : "";
+            }, hasChanges);
+        
+        String untermietzuschlagHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double umietZuschlag = m.path("umietZuschlag").asDouble(0.0);
+                return umietZuschlag > 0 ? String.format("%.2f €", umietZuschlag) : "";
+            }, hasChanges);
+        
+        String sonstZuschlagHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double sonstZuschlag = m.path("sonstZuschlag").asDouble(0.0);
+                return sonstZuschlag > 0 ? String.format("%.2f €", sonstZuschlag) : "";
+            }, hasChanges);
+        
+        String leistungenDritterHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double beitragDritter = m.path("beitragDritter").asDouble(0.0);
+                return beitragDritter > 0 ? String.format("%.2f €", beitragDritter) : "";
+            }, hasChanges);
+        
+        // Rechte Spalte - Pauschalen
+        String heizungPauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("heizungP").asText(""), hasChanges);
+        
+        String warmwasserPauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("warmwasserP").asText(""), hasChanges);
+        
+        String energiePauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("energieP").asText(""), hasChanges);
+        
+        String stellplatzPauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("stellplatzP").asText(""), hasChanges);
+        
+        String moeblierungPauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("moeblierungP").asText(""), hasChanges);
+        
+        String gewerbePauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("gewerbeZuschlagP").asText(""), hasChanges);
+        
+        String sonstPauschaleHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> m.path("sonstZuschlagBeschr").asText(""), hasChanges);
+        
+        // 4. Spalte - Gewerbefläche
+        String gewerbeflaecheHtml = htmlService.buildMieteFieldHtml(rightMiete, leftMiete,
+            m -> {
+                double gewerbeFlaeche = m.path("gewerbeFlaeche").asDouble(0.0);
+                return gewerbeFlaeche > 0 ? String.format("%.2f m²", gewerbeFlaeche) : "";
+            }, hasChanges);
+        
+        return new MieteZeile(
+            rowClass, symbol, symbolClass,
+            gesamtmieteHtml, zuschlägeHtml, artZuschlagHtml,
+            jhrlFernwaermeHtml, jhrlFernwarmwasserHtml, jhrlFernwUndWHtml,
+            heizungHtml, warmwasserHtml, kostenHhEnergieHtml, stellplatzHtml,
+            moeblierungszuschlagHtml, gewerbezuschlagHtml, untermietzuschlagHtml,
+            sonstZuschlagHtml, leistungenDritterHtml,
+            heizungPauschaleHtml, warmwasserPauschaleHtml, energiePauschaleHtml,
+            stellplatzPauschaleHtml, moeblierungPauschaleHtml, gewerbePauschaleHtml,
+            sonstPauschaleHtml, gewerbeflaecheHtml
+        );
+    }
 }

+ 52 - 0
src/main/java/com/tsystems/inwo/vgl/view/MieteZeile.java

@@ -0,0 +1,52 @@
+package com.tsystems.inwo.vgl.view;
+
+/**
+ * View-Klasse für Miete-Darstellung mit dreispaltigem Layout
+ */
+public record MieteZeile(
+    String rowClass,
+    String symbol,
+    String symbolClass,
+    
+    // Linke Spalte - Allgemeine Daten
+    String gesamtmieteHtml,
+    String zuschlägeHtml,
+    String artZuschlagHtml,
+    String jhrlFernwaermeHtml,
+    String jhrlFernwarmwasserHtml,
+    String jhrlFernwUndWHtml,
+    
+    // Mittlere Spalte - Abzüge
+    String heizungHtml,
+    String warmwasserHtml,
+    String kostenHhEnergieHtml,
+    String stellplatzHtml,
+    String moeblierungszuschlagHtml,
+    String gewerbezuschlagHtml,
+    String untermietzuschlagHtml,
+    String sonstZuschlagHtml,
+    String leistungenDritterHtml,
+    
+    // Rechte Spalte - Pauschalen
+    String heizungPauschaleHtml,
+    String warmwasserPauschaleHtml,
+    String energiePauschaleHtml,
+    String stellplatzPauschaleHtml,
+    String moeblierungPauschaleHtml,
+    String gewerbePauschaleHtml,
+    String sonstPauschaleHtml,
+    
+    // 4. Spalte - Gewerbefläche
+    String gewerbeflaecheHtml
+) {
+    
+    public static MieteZeile empty() {
+        return new MieteZeile(
+            "", "", "",
+            "", "", "", "", "", "",
+            "", "", "", "", "", "", "", "", "",
+            "", "", "", "", "", "", "",
+            ""
+        );
+    }
+}

+ 15 - 0
src/main/java/com/tsystems/inwo/vgl/view/MitbewohnerZeile.java

@@ -0,0 +1,15 @@
+package com.tsystems.inwo.vgl.view;
+
+public record MitbewohnerZeile(
+    String symbol,
+    String symbolClass,
+    String rowClass,
+    String nameHtml,
+    String vornameHtml,
+    String entgeltHtml,
+    String sonstHtml,
+    String heizungHtml,
+    String heizungPHtml,
+    String warmwasserHtml,
+    String warmwasserPHtml
+) {}

+ 18 - 0
src/main/java/com/tsystems/inwo/vgl/view/UntermieterZeile.java

@@ -0,0 +1,18 @@
+package com.tsystems.inwo.vgl.view;
+
+public record UntermieterZeile(
+    String symbol,
+    String symbolClass,
+    String rowClass,
+    String nameHtml,
+    String vornameHtml,
+    String mieteHtml,
+    String flaecheHtml,
+    String sonstHtml,
+    String heizungHtml,
+    String heizungPHtml,
+    String warmwasserHtml,
+    String warmwasserPHtml,
+    String moeblierungHtml,
+    String moeblierungPHtml
+) {}

+ 24 - 0
src/main/java/com/tsystems/inwo/vgl/view/WohnungZeile.java

@@ -0,0 +1,24 @@
+package com.tsystems.inwo.vgl.view;
+
+/**
+ * View-Zeile für Wohnungsdaten-Vergleich
+ * Entspricht dem Header-Bereich auf der Miete-Seite (aus ausstattung)
+ */
+public record WohnungZeile(
+    String symbol,
+    String symbolClass,
+    String rowClass,
+    String mietverhaeltnisHtml,
+    String flaecheHtml,
+    String badHtml,
+    String nameEigHtml,
+    String telefonEigHtml,
+    String sozBauJnHtml,
+    String fertigDatHtml,
+    String heizungHtml,
+    String adresseEigHtml
+) {
+    public WohnungZeile(String symbol, String symbolClass, String rowClass) {
+        this(symbol, symbolClass, rowClass, "", "", "", "", "", "", "", "", "");
+    }
+}

+ 1 - 0
src/main/java/com/tsystems/inwo/vgl/web/HhmEinkommenController.java

@@ -120,6 +120,7 @@ public class HhmEinkommenController {
         model.addAttribute("freibetragZeilen", context.freibetragZeilen);
         model.addAttribute("kitaZeilen", context.kitaZeilen);
         model.addAttribute("activeKunde", activeKunde);
+        model.addAttribute("pageTitle", "Haushaltsmitglieder/Einkommen");
         return "hhm-einkommen";
     }
 

+ 15 - 0
src/main/java/com/tsystems/inwo/vgl/web/MieteController.java

@@ -88,6 +88,16 @@ public class MieteController {
             if (leftZeitraumId != null) leftZeitraum = jsonNavigationService.findZeitraumById(jsonNavigationService.getRootOfVorgang(left, leftVorgangId), leftZeitraumId);
         }
 
+        // --- Wohnungsvergleich ---
+        WohnungZeile wohnungZeile = dataExtractor.buildWohnungZeile(rightZeitraum, leftZeitraum);
+
+        // --- Mietedaten ---
+        MieteZeile mieteZeile = dataExtractor.buildMieteZeile(rightZeitraum, leftZeitraum);
+
+        // --- Untermieter und Mitbewohner ---
+        List<UntermieterZeile> untermieterZeilen = dataExtractor.buildUntermieterZeilen(rightZeitraum, leftZeitraum);
+        List<MitbewohnerZeile> mitbewohnerZeilen = dataExtractor.buildMitbewohnerZeilen(rightZeitraum, leftZeitraum);
+
         // --- Model füllen & rendern ---
         model.addAttribute("leftJson", leftJson);
         model.addAttribute("rightJson", rightJson);
@@ -101,7 +111,12 @@ public class MieteController {
         model.addAttribute("rightZeitraumId", rightZeitraumId);
         model.addAttribute("stammvorgangRight", stammvorgangRight);
         model.addAttribute("stammakteRight", stammakteRight);
+        model.addAttribute("wohnungZeile", wohnungZeile);
+        model.addAttribute("mieteZeile", mieteZeile);
+        model.addAttribute("untermieterZeilen", untermieterZeilen);
+        model.addAttribute("mitbewohnerZeilen", mitbewohnerZeilen);
         model.addAttribute("activeKunde", activeKunde);
+        model.addAttribute("pageTitle", "Miete-/Wohnungsdaten");
         return "miete";
     }
 

+ 310 - 0
src/main/resources/static/css/global.css

@@ -0,0 +1,310 @@
+/* =============================================================================
+   DIWO Vergleich - Global CSS
+   =============================================================================
+   
+   Gemeinsame Stile die von allen Seiten verwendet werden:
+   - CSS Custom Properties (Variablen)
+   - Reset & Base Styles
+   - Layout Components
+   - Form Elements
+   - Button Styles
+   - Table Styles
+   - Section Components (Collapsible Sections)
+   ============================================================================= */
+
+/* =============================================================================
+   CSS CUSTOM PROPERTIES (VARIABLEN)
+   ============================================================================= */
+:root {
+  /* Standard Farben mit verbessertem Kontrast */
+  --rot-hell: #ffe6e6;
+  --gelb-hell: #fff8db;
+  --gruen-hell: #e8f5e8;
+  --rot: #b30000;
+  --gelb: #9d5502;
+  --gruen: #004d26;
+  --weiss: #ffffff;
+  --schwarz: #000000;
+  --grau-hell: #f7f9fa;
+  --grau-stark: #d7d8d9;
+  --berlin-rot: #e20613;
+  --diwo-blau: #145c9e;
+  --diwo-blau-hell: #cfe8fc;
+  
+  /* Focus Ring Farbe für bessere Sichtbarkeit */
+  --focus-color: #0066cc;
+  --focus-outline: 3px solid var(--focus-color);
+}
+
+/* Alternative Farben mit besserem Kontrast */
+:root[data-color-scheme="alternative"] {
+  --rot-hell: #ffe6f5;
+  --gelb-hell: #ffeccc;
+  --gruen-hell: #e6f9ff;
+  --rot: #cc0044;
+  --gelb: #b8620e;
+  --gruen: #008a8a;
+}
+
+/* =============================================================================
+   RESET & BASE STYLES
+   ============================================================================= */
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  padding: 16px;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  font: 14px/1.5 system-ui, sans-serif;
+}
+
+h1, h2, h3 {
+  margin: 0.2rem 0 0.6rem;
+  line-height: 1.3;
+}
+
+/* Focus-Stile für bessere Keyboard-Navigation */
+*:focus {
+  outline: var(--focus-outline);
+  outline-offset: 2px;
+}
+
+button:focus,
+select:focus,
+input:focus,
+.person-row:focus,
+.section-header:focus {
+  outline: var(--focus-outline);
+  outline-offset: 2px;
+}
+
+tr:focus {
+  outline: var(--focus-outline);
+  outline-offset: 1px;
+  position: relative;
+  z-index: 1;
+}
+
+/* =============================================================================
+   LAYOUT COMPONENTS
+   ============================================================================= */
+.row {
+  display: flex;
+  gap: 0.5rem;
+  align-items: center;
+}
+
+.grid-2 {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 12px;
+}
+
+.grid-2 .full {
+  grid-column: 1/-1;
+}
+
+.error-box {
+  background: var(--rot-hell);
+  border-left: 4px solid var(--rot);
+  padding: 0.6rem;
+  margin: 0.6rem 0;
+  border-radius: 4px;
+}
+
+/* =============================================================================
+   FORM ELEMENTS
+   ============================================================================= */
+textarea,
+input,
+select {
+  width: 100%;
+  padding: 0.5rem;
+  background: var(--weiss);
+  color: var(--schwarz);
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px;
+  font-size: 14px;
+}
+
+textarea:hover,
+input:hover,
+select:hover {
+  border-color: var(--diwo-blau);
+}
+
+.control-group {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.control-group label {
+  font-size: 13px;
+  font-weight: bold;
+  white-space: nowrap;
+}
+
+/* =============================================================================
+   BUTTON STYLES
+   ============================================================================= */
+button {
+  padding: 0.5rem 0.9rem;
+  border: 2px solid var(--grau-stark);
+  background: var(--weiss);
+  cursor: pointer;
+  font-size: 14px;
+  min-height: 44px; 
+  transition: all 0.2s ease;
+}
+
+button:hover {
+  background: var(--grau-hell);
+  border-color: var(--diwo-blau);
+}
+
+button:active {
+  transform: translateY(1px);
+}
+
+/* =============================================================================
+   TABLE STYLES
+   ============================================================================= */
+table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0.6rem 0 1rem;
+  border-spacing: 0;
+}
+
+th, td {
+  border: 1px solid var(--grau-stark);
+  padding: 0.5rem;
+  vertical-align: top;
+  text-align: left; 
+}
+
+th {
+  background: var(--grau-stark);
+  position: sticky;
+  top: 0;
+  font-weight: bold;
+  color: var(--schwarz); 
+}
+
+/* Symbol-Spalte */
+.sym {
+  width: 2.5rem;
+  min-width: 2.5rem;
+  max-width: 2.5rem;
+  text-align: center;
+  font-weight: 700;
+  font-size: 16px; 
+}
+
+.sym.plus {
+  color: var(--gruen);
+}
+
+.sym.minus {
+  color: var(--rot);
+}
+
+.sym.warn {
+  color: var(--gelb);
+}
+
+/* Diff-Highlighting für Tabellenzeilen */
+.diff-row-added {
+  background: var(--gruen-hell);
+}
+
+.diff-row-removed {
+  background: var(--rot-hell);
+}
+
+.cell-changed,
+td:has(.cell-changed) {
+  background: var(--gelb-hell);
+}
+
+.cell-changed:not(.sym)::before {
+  content: "! ";
+  color: var(--gelb);
+  font-weight: bold;
+  font-size: 0.9em;
+  margin-right: 4px;
+}
+
+.old {
+  display: block;
+  font-size: 0.85em;
+  margin-top: 0.2rem;
+  opacity: 0.9;
+  font-style: italic;
+}
+
+/* =============================================================================
+   SECTION COMPONENTS (COLLAPSIBLE SECTIONS)
+   ============================================================================= */
+.section-container {
+  margin: 0.6rem 0 1rem;
+}
+
+.section-header {
+  background: var(--grau-stark);
+  padding: 12px 16px;
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px 4px 0 0;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  user-select: none;
+  transition: all 0.2s ease;
+  min-height: 44px;
+}
+
+.section-header:hover {
+  background: #c5c6c7;
+  border-color: var(--diwo-blau);
+}
+
+.section-header h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: bold;
+}
+
+.toggle-icon {
+  font-size: 14px;
+  transition: transform 0.2s ease;
+  font-weight: bold;
+}
+
+.toggle-icon.collapsed {
+  transform: rotate(-90deg);
+}
+
+.section-content {
+  border: 2px solid var(--grau-stark);
+  border-top: none;
+  border-radius: 0 0 4px 4px;
+  transition: max-height 0.3s ease;
+  overflow: visible;
+  padding: 4px;
+}
+
+.section-content.collapsed {
+  display: none;
+}
+
+.section-content table {
+  margin: 0;
+  border: none;
+}

+ 145 - 0
src/main/resources/static/css/header.css

@@ -0,0 +1,145 @@
+/* =============================================================================
+   DIWO Vergleich - Header CSS
+   =============================================================================
+   
+   Stile speziell für Header-Komponenten:
+   - DIWO Header
+   - Kunde Logo
+   - Vergleichssteuerungen
+   - Navigation
+   ============================================================================= */
+
+/* =============================================================================
+   DIWO HEADER COMPONENTS
+   ============================================================================= */
+.diwo-header {
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  padding: 12px 16px;
+  margin-bottom: 16px;
+  border-radius: 4px;
+}
+
+.header-row-1 {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 8px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid var(--grau-stark);
+}
+
+.kunde-logo {
+  width: auto;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.kunde-logo-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.kunde-logo-image {
+  max-width: auto;
+  max-height: 50px;
+  object-fit: contain;
+  border-radius: 4px;
+}
+
+.diwo-title {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.diwo-title h1 {
+  font-size: 24px;
+  font-weight: bold;
+  margin: 0;
+  color: var(--diwo-blau);
+}
+
+.diwo-title h2 {
+  font-size: 18px;
+  font-weight: normal;
+  margin: 0;
+  color: var(--schwarz);
+}
+
+.page-title {
+  font-size: 16px;
+  font-weight: bold;
+  flex: 1;
+  text-align: center;
+}
+
+.header-row-2 {
+  display: grid;
+  grid-template-columns: auto 1fr auto;
+  gap: 20px;
+  margin-bottom: 8px;
+  align-items: center;
+}
+
+.aktenzeichen {
+  font-weight: bold;
+  font-size: 14px;
+}
+
+.antragsteller-info {
+  text-align: center;
+  font-weight: bold;
+}
+
+.location-info {
+  text-align: right;
+  font-size: 13px;
+}
+
+.header-row-3 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 8px;
+  padding-top: 8px;
+  border-top: 1px solid var(--grau-stark);
+}
+
+.vorgang-info {
+  font-size: 13px;
+}
+
+/* =============================================================================
+   VERGLEICHSSTEUERUNGEN
+   ============================================================================= */
+.vergleich-controls {
+  display: flex;
+  align-items: center;
+}
+
+.vergleich-controls-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.vorgang-form,
+.zeitraum-form {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.vorgang-select,
+.zeitraum-select {
+  width: 200px;
+  padding: 4px 8px;
+  font-size: 13px;
+  min-height: 32px;
+}

+ 178 - 0
src/main/resources/static/css/hhm-einkommen.css

@@ -0,0 +1,178 @@
+/* =============================================================================
+   DIWO Vergleich - HHM Einkommen CSS
+   =============================================================================
+   
+   Stile speziell für HHM-Einkommen-Seite:
+   - Expandable Tables für Personendetails
+   - Person Row Interaktionen
+   - Layout für Freibeträge und Kinderbetreuung
+   ============================================================================= */
+
+/* =============================================================================
+   PERSON ROW INTERAKTIONEN
+   ============================================================================= */
+/* Klickbare Tabellenzeilen für Personenauswahl */
+.person-row {
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+  position: relative;
+}
+
+.person-row:hover {
+  background-color: var(--diwo-blau-hell);
+}
+
+.person-row:focus {
+  background-color: var(--diwo-blau-hell);
+  outline: var(--focus-outline);
+  outline-offset: 1px;
+  z-index: 1;
+}
+
+/* Ausgewählte Person - nur auf HHM-Seite anzeigen */
+.person-selected {
+  outline: none;
+}
+
+.hhm-page .person-selected {
+  outline: 3px solid var(--diwo-blau);
+  outline-offset: -3px;
+}
+
+/* =============================================================================
+   EXPANDABLE TABLES FÜR PERSONENDETAILS
+   ============================================================================= */
+.expandable-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0.6rem 0 1rem;
+  border-spacing: 0;
+}
+
+.expand-col {
+  width: 40px;
+  min-width: 40px;
+  max-width: 40px;
+  text-align: center;
+  padding: 0.25rem;
+}
+
+/* Aufklapp-Button für Personendetails */
+.expand-btn {
+  background: none;
+  border: 1px solid var(--grau-stark);
+  cursor: pointer;
+  font-size: 14px;
+  padding: 4px 6px;
+  border-radius: 3px;
+  transition: all 0.2s ease;
+  min-height: 28px;
+  min-width: 28px;
+  color: var(--diwo-blau);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.expand-btn:hover {
+  background: var(--diwo-blau-hell);
+  border-color: var(--diwo-blau);
+}
+
+.expand-btn:focus {
+  outline: 2px solid var(--focus-color);
+  outline-offset: 1px;
+}
+
+.expand-btn span {
+  transition: transform 0.2s ease;
+  font-weight: bold;
+}
+
+.expand-btn[aria-expanded="true"] span {
+  transform: rotate(90deg);
+}
+
+.expand-btn:focus-visible {
+  outline: 3px solid var(--focus-color);
+  outline-offset: 2px;
+}
+
+/* Detail-Zeilen für Personendetails */
+.person-details-row {
+  transition: all 0.3s ease;
+}
+
+.person-details-row.hidden {
+  display: none;
+}
+
+.details-cell {
+  padding: 16px;
+  background: var(--grau-stark) !important;
+  border-left: 4px solid var(--diwo-blau);
+}
+
+.details-container {
+  max-width: 100%;
+}
+
+.details-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin-top: 8px;
+  font-size: 14px;
+}
+
+.details-table td {
+  border: 1px solid var(--grau-stark);
+  padding: 6px 8px;
+  vertical-align: top;
+  background: var(--weiss);
+}
+
+.detail-label {
+  font-weight: bold;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  width: 12.5%;
+  min-width: 80px;
+}
+
+/* =============================================================================
+   LAYOUT FÜR FREIBETRÄGE UND KINDERBETREUUNG
+   ============================================================================= */
+.freibetrag-container {
+  display: flex;
+  gap: 16px;
+  margin: 0.6rem 0 1rem;
+}
+
+.freibetrag-section {
+  flex: 1;
+  min-width: 0;
+}
+
+.freibetrag-section h3 {
+  margin: 0 0 0.6rem;
+  font-size: 16px;
+}
+
+.kinderbetreuung-container {
+  max-width: 50%;
+}
+
+/* Bei kleinen Bildschirmen: Untereinander anordnen */
+@media (max-width: 1200px) {
+  .freibetrag-container {
+    flex-direction: column;
+  }
+
+  .kinderbetreuung-container {
+    max-width: 100%;
+  }
+}

+ 134 - 0
src/main/resources/static/css/legend.css

@@ -0,0 +1,134 @@
+/* =============================================================================
+   DIWO Vergleich - Legend CSS
+   =============================================================================
+   
+   Stile speziell für Legende-Komponenten:
+   - Vergleichslegende
+   - Color Picker
+   - Legende Items
+   ============================================================================= */
+
+/* =============================================================================
+   VERGLEICHSLEGENDE
+   ============================================================================= */
+.vergleich-legende {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin: 12px 0;
+  padding: 8px 12px;
+  background: var(--grau-hell);
+  border: 1px solid var(--grau-stark);
+  border-radius: 4px;
+  font-size: 13px;
+}
+
+.legende-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 8px;
+  border-radius: 3px;
+}
+
+.legende-item:has(.plus) {
+  background: var(--gruen-hell);
+  border: 1px solid var(--gruen); 
+}
+
+.legende-item:has(.minus) {
+  background: var(--rot-hell);
+  border: 1px solid var(--rot);
+}
+
+.legende-item:has(.warn) {
+  background: var(--gelb-hell);
+  border: 1px solid var(--gelb);
+}
+
+.legende-title {
+  margin-right: 8px;
+  font-size: 13px;
+}
+
+.legende-spacer {
+  color: var(--grau-stark);
+  font-weight: bold;
+}
+
+.selected-indicator-text {
+  border: 3px solid var(--diwo-blau);
+  padding: 2px 4px;
+  border-radius: 2px;
+  display: inline-block;
+}
+
+/* Legende-Item für ausgewählte Person nur auf HHM-Seite anzeigen */
+.hhm-only {
+  display: none;
+}
+
+.hhm-page .hhm-only {
+  display: flex;
+}
+
+/* =============================================================================
+   COLOR PICKER
+   ============================================================================= */
+.color-picker-container {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin-left: auto;
+}
+
+.color-picker-toggle {
+  background: none;
+  border: 2px solid var(--grau-stark);
+  cursor: pointer;
+  font-size: 16px;
+  padding: 4px;
+  border-radius: 4px;
+  transition: all 0.2s ease;
+  min-height: 32px;
+  min-width: 32px;
+}
+
+.color-picker-toggle:hover {
+  background: var(--grau-stark);
+  border-color: var(--diwo-blau);
+}
+
+.color-picker-popup {
+  position: absolute;
+  top: 100%;
+  right: 0;
+  background: var(--weiss);
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px;
+  padding: 12px;
+  box-shadow: 0 4px 12px rgba(20, 92, 158, 0.2);
+  z-index: 1000;
+  font-size: 13px;
+  min-width: 160px;
+}
+
+.color-picker-popup.hidden {
+  display: none;
+}
+
+.color-picker-popup label {
+  font-weight: bold;
+  margin-bottom: 6px;
+  display: block;
+}
+
+.color-scheme-select {
+  width: 100%;
+  padding: 6px 8px;
+  font-size: 13px;
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px;
+  min-height: 32px;
+}

+ 136 - 0
src/main/resources/static/css/miete.css

@@ -0,0 +1,136 @@
+/* =============================================================================
+   DIWO Vergleich - Miete CSS
+   =============================================================================
+   
+   Stile speziell für Miete-Seite:
+   - Wohnung Grid Table
+   - Miete Grid Table
+   - Responsive Design für Miete-Tabellen
+   ============================================================================= */
+
+/* =============================================================================
+   WOHNUNG GRID TABLE
+   ============================================================================= */
+.wohnung-grid-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0;
+  font-size: 14px;
+}
+
+.wohnung-grid-table td {
+  border: 1px solid var(--grau-stark);
+  padding: 8px 12px;
+  vertical-align: middle;
+}
+
+.label-cell {
+  font-weight: bold;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  white-space: nowrap;
+}
+
+.value-cell {
+  background: var(--weiss);
+}
+
+/* =============================================================================
+   MIETE GRID TABLE
+   ============================================================================= */
+.miete-grid-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0;
+  font-size: 14px;
+  table-layout: fixed;
+}
+
+.miete-grid-table td,
+.miete-grid-table th {
+  border: 1px solid var(--grau-stark);
+  padding: 8px;
+  vertical-align: middle;
+  text-align: left;
+}
+
+/* Header-Zellen in Miete-Tabelle */
+.miete-grid-table .miete-header-cell {
+  font-weight: bold;
+  background: var(--grau-stark);
+  color: var(--schwarz);
+  text-align: left;
+}
+
+/* Verschiedene Zelltypen in Miete-Tabelle */
+.miete-grid-table .label-cell {
+  font-weight: bold;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  white-space: nowrap;
+  width: 15%;
+}
+
+.miete-grid-table .value-cell {
+  background: var(--weiss);
+  text-align: left;
+  width: 15%;
+}
+
+.miete-grid-table .pauschale-cell {
+  background: var(--weiss);
+  text-align: left;
+  font-style: normal;
+  color: var(--schwarz);
+  font-size: 14px;
+  width: 15%;
+}
+
+.miete-grid-table .gewerbefläche-cell {
+  background: var(--weiss);
+  color: var(--schwarz);
+  font-weight: normal;
+  text-align: left;
+}
+
+.miete-grid-table .empty-cell {
+  background: var(--weiss);
+  width: 10%;
+}
+
+.miete-grid-table .section-header-cell {
+  font-weight: bold;
+  color: var(--diwo-blau);
+  background: var(--grau-stark);
+  text-align: left;
+  border-bottom: none;
+}
+
+/* =============================================================================
+   DIFF-HIGHLIGHTING FÜR MIETE-TABELLEN
+   ============================================================================= */
+/* Geänderte Zellen in Miete-Tabellen sollen gelb hervorgehoben werden */
+.miete-grid-table .cell-changed,
+.miete-grid-table td:has(.cell-changed) {
+  background: var(--gelb-hell) !important;
+}
+
+.miete-grid-table .value-cell.cell-changed,
+.miete-grid-table .pauschale-cell.cell-changed,
+.miete-grid-table .gewerbefläche-cell.cell-changed {
+  background: var(--gelb-hell) !important;
+}
+
+/* Wohnung-Tabelle Highlighting */
+.wohnung-grid-table .cell-changed,
+.wohnung-grid-table td:has(.cell-changed) {
+  background: var(--gelb-hell) !important;
+}
+
+.wohnung-grid-table .value-cell.cell-changed {
+  background: var(--gelb-hell) !important;
+}

+ 142 - 6
src/main/resources/static/example-miete-alt.json

@@ -6,6 +6,7 @@
     "anschrift": {
       "strasse": "Musterstraße",
       "hausnummer": "123",
+      "zusatz": "1. OG links",
       "plz": "10115",
       "ort": "Berlin"
     },
@@ -25,17 +26,31 @@
               "id": "miete-1",
               "miete": 650.00,
               "mieteAlt": 600.00,
+              "sonstZuschlag": 25.00,
+              "sonstZuschlagBeschr": "Hausmeisterservice",
+              "beitragDritter": 0.00,
               "heizung": 120.00,
               "heizungP": "Fernwärme",
               "warmwasser": 45.00,
               "warmwasserP": "zentral",
               "moeblierung": 0.00,
-              "sonstZuschlag": 25.00,
-              "sonstZuschlagBeschr": "Hausmeisterservice",
-              "beitragDritter": 0.00,
+              "moeblierungP": "unmöbliert",
+              "umietZuschlag": 35.00,
+              "gewerbeZuschlag": 25.00,
+              "gewerbeZuschlagP": "Büronutzung",
+              "gewerbeFlaeche": 8.00,
+              "jhrlFernwarmw": 540.00,
+              "jhrlFernwaerme": 1440.00,
+              "antMietWw": 45.00,
+              "antBrutWw": 50.00,
+              "jhrlFernWUndW": 1980.00,
+              "lstgAufenthaltPerson": 0.00,
+              "sonstWogFaehigBetrag": 0.00,
+              "sonstWogFaehigText": "",
               "energie": 80.00,
               "energieP": "Strom",
               "stellplatz": 0.00,
+              "stellplatzP": "kein Stellplatz",
               "untermieter": [
                 {
                   "miId": "miete-1",
@@ -49,7 +64,23 @@
                   "warmwasser": 15.00,
                   "warmwasserP": "anteilig",
                   "moeblierung": 0.00,
+                  "moeblierungP": "unmöbliert",
                   "sonst": 10.00
+                },
+                {
+                  "miId": "miete-1",
+                  "lfdnr": 2,
+                  "name": "Johnson",
+                  "vorname": "Mike",
+                  "flaeche": 15.0,
+                  "miete": 150.00,
+                  "heizung": 20.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 10.00,
+                  "warmwasserP": "anteilig",
+                  "moeblierung": 25.00,
+                  "moeblierungP": "teilmöbliert",
+                  "sonst": 5.00
                 }
               ],
               "mitbewohner": [
@@ -64,6 +95,18 @@
                   "warmwasser": 10.00,
                   "warmwasserP": "anteilig",
                   "sonst": 5.00
+                },
+                {
+                  "miId": "miete-1",
+                  "lfdnr": 2,
+                  "name": "Fischer",
+                  "vorname": "Sarah",
+                  "entgelt": 120.00,
+                  "heizung": 15.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 8.00,
+                  "warmwasserP": "anteilig",
+                  "sonst": 3.00
                 }
               ]
             },
@@ -91,14 +134,27 @@
               "id": "miete-2",
               "miete": 675.00,
               "mieteAlt": 650.00,
+              "sonstZuschlag": 30.00,
+              "sonstZuschlagBeschr": "Hausmeisterservice + Reinigung",
+              "beitragDritter": 50.00,
               "heizung": 130.00,
               "heizungP": "Fernwärme",
               "warmwasser": 50.00,
               "warmwasserP": "zentral",
               "moeblierung": 0.00,
-              "sonstZuschlag": 30.00,
-              "sonstZuschlagBeschr": "Hausmeisterservice + Reinigung",
-              "beitragDritter": 0.00,
+              "moeblierungP": "unmöbliert",
+              "umietZuschlag": 45.00,
+              "gewerbeZuschlag": 35.00,
+              "gewerbeZuschlagP": "Homeoffice + Lagerraum",
+              "gewerbeFlaeche": 12.00,
+              "jhrlFernwarmw": 600.00,
+              "jhrlFernwaerme": 1560.00,
+              "antMietWw": 50.00,
+              "antBrutWw": 55.00,
+              "jhrlFernWUndW": 2160.00,
+              "lstgAufenthaltPerson": 0.00,
+              "sonstWogFaehigBetrag": 25.00,
+              "sonstWogFaehigText": "Balkonnutzung",
               "energie": 85.00,
               "energieP": "Strom",
               "stellplatz": 25.00,
@@ -121,6 +177,86 @@
               "wofgJn": true,
               "foerderungJn": false
             }
+          },
+          {
+            "id": "zeitraum-3",
+            "beginn": "2024-07-01",
+            "miete": {
+              "id": "miete-3",
+              "miete": 700.00,
+              "mieteAlt": 675.00,
+              "sonstZuschlag": 40.00,
+              "sonstZuschlagBeschr": "Hausmeisterservice + Reinigung + Gartenpflege",
+              "beitragDritter": 75.00,
+              "heizung": 140.00,
+              "heizungP": "Fernwärme + Zusatzheizung",
+              "warmwasser": 60.00,
+              "warmwasserP": "zentral + dezentral",
+              "moeblierung": 100.00,
+              "moeblierungP": "vollmöbliert",
+              "umietZuschlag": 55.00,
+              "gewerbeZuschlag": 60.00,
+              "gewerbeZuschlagP": "Praxis + Homeoffice-Nutzung",
+              "gewerbeFlaeche": 18.50,
+              "jhrlFernwarmw": 720.00,
+              "jhrlFernwaerme": 1680.00,
+              "antMietWw": 60.00,
+              "antBrutWw": 65.00,
+              "jhrlFernWUndW": 2400.00,
+              "lstgAufenthaltPerson": 200.00,
+              "sonstWogFaehigBetrag": 50.00,
+              "sonstWogFaehigText": "Balkon + Gartenanteil",
+              "energie": 95.00,
+              "energieP": "Strom + Gas + Solar",
+              "stellplatz": 35.00,
+              "stellplatzP": "Tiefgarage mit Ladestation",
+              "untermieter": [
+                {
+                  "miId": "miete-3",
+                  "lfdnr": 1,
+                  "name": "Martinez",
+                  "vorname": "Carlos",
+                  "flaeche": 18.0,
+                  "miete": 180.00,
+                  "heizung": 25.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 12.00,
+                  "warmwasserP": "anteilig",
+                  "moeblierung": 30.00,
+                  "moeblierungP": "möbliert",
+                  "sonst": 8.00
+                }
+              ],
+              "mitbewohner": [
+                {
+                  "miId": "miete-3",
+                  "lfdnr": 1,
+                  "name": "Liu",
+                  "vorname": "Wei",
+                  "entgelt": 140.00,
+                  "heizung": 18.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 9.00,
+                  "warmwasserP": "anteilig",
+                  "sonst": 4.00
+                }
+              ]
+            },
+            "ausstattung": {
+              "id": 100003,
+              "flaeche": 78.20,
+              "heizung": "ZH",
+              "bad": "V",
+              "sozBauJn": false,
+              "mietverhaeltnis": "EM",
+              "eigentum": "PRI",
+              "fertigDat": "2015-09-10",
+              "nameEig": "Moderne Wohnungen GmbH & Co. KG",
+              "adresseEig": "Friedrichstraße 200, 10117 Berlin",
+              "telefonEig": "030-55555555",
+              "wofgJn": false,
+              "foerderungJn": true
+            }
           }
         ]
       }

+ 205 - 6
src/main/resources/static/example-miete-neu.json

@@ -6,7 +6,7 @@
     "anschrift": {
       "strasse": "Musterstraße",
       "hausnummer": "123",
-      "zusatz": "2. OG",
+      "zusatz": "2. OG rechts",
       "plz": "10115",
       "ort": "Berlin"
     },
@@ -26,21 +26,31 @@
               "id": "miete-1",
               "miete": 680.00,
               "mieteAlt": 650.00,
+              "sonstZuschlag": 35.00,
+              "sonstZuschlagBeschr": "Hausmeisterservice + Garten",
+              "beitragDritter": 100.00,
               "heizung": 135.00,
               "heizungP": "Fernwärme",
               "warmwasser": 55.00,
               "warmwasserP": "zentral",
               "moeblierung": 50.00,
               "moeblierungP": "Küche möbliert",
-              "sonstZuschlag": 35.00,
-              "sonstZuschlagBeschr": "Hausmeisterservice + Garten",
-              "beitragDritter": 100.00,
+              "umietZuschlag": 40.00,
+              "gewerbeZuschlag": 30.00,
+              "gewerbeZuschlagP": "Homeoffice-Arbeitsplatz",
+              "gewerbeFlaeche": 10.00,
+              "jhrlFernwarmw": 660.00,
+              "jhrlFernwaerme": 1620.00,
+              "antMietWw": 55.00,
+              "antBrutWw": 60.00,
+              "jhrlFernWUndW": 2280.00,
+              "lstgAufenthaltPerson": 0.00,
+              "sonstWogFaehigBetrag": 30.00,
+              "sonstWogFaehigText": "Gartennutzung",
               "energie": 90.00,
               "energieP": "Strom + Gas",
               "stellplatz": 30.00,
               "stellplatzP": "Außenstellplatz",
-              "gewerbeZuschlag": 0.00,
-              "umietZuschlag": 0.00,
               "untermieter": [
                 {
                   "miId": "miete-1",
@@ -69,6 +79,7 @@
                   "warmwasser": 12.00,
                   "warmwasserP": "anteilig",
                   "moeblierung": 0.00,
+                  "moeblierungP": "unmöbliert",
                   "sonst": 8.00
                 }
               ],
@@ -102,6 +113,194 @@
               "wofgJn": false,
               "foerderungJn": true
             }
+          },
+          {
+            "id": "zeitraum-2",
+            "beginn": "2024-01-01",
+            "ende": "2024-06-30",
+            "miete": {
+              "id": "miete-2",
+              "miete": 720.00,
+              "mieteAlt": 680.00,
+              "sonstZuschlag": 45.00,
+              "sonstZuschlagBeschr": "Hausmeisterservice + Garten + Reinigung",
+              "beitragDritter": 120.00,
+              "heizung": 150.00,
+              "heizungP": "Fernwärme + Solar",
+              "warmwasser": 65.00,
+              "warmwasserP": "zentral + Solar",
+              "moeblierung": 75.00,
+              "moeblierungP": "Küche + Wohnzimmer möbliert",
+              "umietZuschlag": 50.00,
+              "gewerbeZuschlag": 45.00,
+              "gewerbeZuschlagP": "Büronutzung + Lager",
+              "gewerbeFlaeche": 15.50,
+              "jhrlFernwarmw": 780.00,
+              "jhrlFernwaerme": 1800.00,
+              "antMietWw": 65.00,
+              "antBrutWw": 70.00,
+              "jhrlFernWUndW": 2580.00,
+              "lstgAufenthaltPerson": 150.00,
+              "sonstWogFaehigBetrag": 40.00,
+              "sonstWogFaehigText": "Balkon + Gartennutzung + Keller",
+              "energie": 105.00,
+              "energieP": "Strom + Gas + Ökostrom",
+              "stellplatz": 40.00,
+              "stellplatzP": "Tiefgarage",
+              "untermieter": [
+                {
+                  "miId": "miete-2",
+                  "lfdnr": 1,
+                  "name": "Chen",
+                  "vorname": "Li",
+                  "flaeche": 22.0,
+                  "miete": 220.00,
+                  "heizung": 35.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 18.00,
+                  "warmwasserP": "anteilig",
+                  "moeblierung": 40.00,
+                  "moeblierungP": "vollmöbliert",
+                  "sonst": 12.00
+                }
+              ],
+              "mitbewohner": [
+                {
+                  "miId": "miete-2",
+                  "lfdnr": 1,
+                  "name": "Garcia",
+                  "vorname": "Maria",
+                  "entgelt": 160.00,
+                  "heizung": 22.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 13.00,
+                  "warmwasserP": "anteilig",
+                  "sonst": 7.00
+                },
+                {
+                  "miId": "miete-2",
+                  "lfdnr": 2,
+                  "name": "Brown",
+                  "vorname": "James",
+                  "entgelt": 170.00,
+                  "heizung": 24.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 14.00,
+                  "warmwasserP": "anteilig",
+                  "sonst": 8.00
+                }
+              ]
+            },
+            "ausstattung": {
+              "id": 200002,
+              "flaeche": 78.90,
+              "heizung": "ZH",
+              "bad": "V",
+              "sozBauJn": false,
+              "mietverhaeltnis": "EM",
+              "eigentum": "PRI",
+              "fertigDat": "2010-06-20",
+              "nameEig": "Müller Immobilien GmbH",
+              "adresseEig": "Potsdamer Platz 5, 10785 Berlin",
+              "telefonEig": "030-98765432",
+              "wofgJn": false,
+              "foerderungJn": true
+            }
+          },
+          {
+            "id": "zeitraum-3",
+            "beginn": "2024-07-01",
+            "miete": {
+              "id": "miete-3",
+              "miete": 750.00,
+              "mieteAlt": 720.00,
+              "sonstZuschlag": 55.00,
+              "sonstZuschlagBeschr": "Vollservice + Premium-Ausstattung",
+              "beitragDritter": 150.00,
+              "heizung": 160.00,
+              "heizungP": "Fernwärme + Solar + Wärmepumpe",
+              "warmwasser": 75.00,
+              "warmwasserP": "zentral + Solar + dezentral",
+              "moeblierung": 120.00,
+              "moeblierungP": "vollständig möbliert + Elektrogeräte",
+              "umietZuschlag": 65.00,
+              "gewerbeZuschlag": 95.00,
+              "gewerbeZuschlagP": "Büro + Praxis-Nutzung + Konferenzraum",
+              "gewerbeFlaeche": 32.50,
+              "jhrlFernwarmw": 900.00,
+              "jhrlFernwaerme": 1920.00,
+              "antMietWw": 75.00,
+              "antBrutWw": 80.00,
+              "jhrlFernWUndW": 2820.00,
+              "lstgAufenthaltPerson": 300.00,
+              "sonstWogFaehigBetrag": 80.00,
+              "sonstWogFaehigText": "Dachterrasse + Gartenanteil + Keller + Abstellraum",
+              "energie": 125.00,
+              "energieP": "Vollökostrom + Gas + Solar + E-Mobility",
+              "stellplatz": 60.00,
+              "stellplatzP": "Duplex-Tiefgarage mit E-Ladestation + Überdachung",
+              "untermieter": [
+                {
+                  "miId": "miete-3",
+                  "lfdnr": 1,
+                  "name": "Andersen",
+                  "vorname": "Lars",
+                  "flaeche": 28.0,
+                  "miete": 280.00,
+                  "heizung": 45.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 22.00,
+                  "warmwasserP": "anteilig",
+                  "moeblierung": 60.00,
+                  "moeblierungP": "Designer-Möbel",
+                  "sonst": 18.00
+                },
+                {
+                  "miId": "miete-3",
+                  "lfdnr": 2,
+                  "name": "Kowalski",
+                  "vorname": "Anna",
+                  "flaeche": 20.0,
+                  "miete": 200.00,
+                  "heizung": 32.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 16.00,
+                  "warmwasserP": "anteilig",
+                  "moeblierung": 35.00,
+                  "moeblierungP": "standard möbliert",
+                  "sonst": 10.00
+                }
+              ],
+              "mitbewohner": [
+                {
+                  "miId": "miete-3",
+                  "lfdnr": 1,
+                  "name": "Rossi",
+                  "vorname": "Marco",
+                  "entgelt": 200.00,
+                  "heizung": 28.00,
+                  "heizungP": "anteilig",
+                  "warmwasser": 16.00,
+                  "warmwasserP": "anteilig",
+                  "sonst": 12.00
+                }
+              ]
+            },
+            "ausstattung": {
+              "id": 200003,
+              "flaeche": 95.20,
+              "heizung": "WP",
+              "bad": "L",
+              "sozBauJn": false,
+              "mietverhaeltnis": "EM",
+              "eigentum": "PRI",
+              "fertigDat": "2020-12-15",
+              "nameEig": "Premium Living Berlin GmbH",
+              "adresseEig": "Unter den Linden 77, 10117 Berlin",
+              "telefonEig": "030-11111111",
+              "wofgJn": false,
+              "foerderungJn": false
+            }
           }
         ]
       }

+ 5 - 5
src/main/resources/static/js/ajax-handlers.js

@@ -38,9 +38,9 @@ function changeLeftVorgang(newLeftVorgangId) {
   formData.append('personId', personId);
   formData.append('newLeftVorgangId', newLeftVorgangId);
 
-  console.log('🌐 Sende AJAX-Request an /api/vorgang-change');
+  console.log('🌐 Sende AJAX-Request an /hhm-einkommen/api/vorgang-change');
   
-  fetch('/api/vorgang-change', {
+  fetch('/hhm-einkommen/api/vorgang-change', {
     method: 'POST',
     body: formData
   })
@@ -91,9 +91,9 @@ function changeLeftZeitraum(newLeftZeitraumId) {
   formData.append('personId', document.getElementById('currentPersonId').value);
   formData.append('newLeftZeitraumId', newLeftZeitraumId);
 
-  console.log('🌐 Sende AJAX-Request an /api/zeitraum-change');
+  console.log('🌐 Sende AJAX-Request an /hhm-einkommen/api/zeitraum-change');
 
-  fetch('/api/zeitraum-change', {
+  fetch('/hhm-einkommen/api/zeitraum-change', {
     method: 'POST',
     body: formData
   })
@@ -141,7 +141,7 @@ function selectPersonAjax(personId) {
   formData.append('rightZeitraumId', document.getElementById('rightZeitraumId').value);
   formData.append('personId', personId);
 
-  fetch('/api/person-details', {
+  fetch('/hhm-einkommen/api/person-details', {
     method: 'POST',
     body: formData
   })

+ 540 - 409
src/main/resources/static/styles.css

@@ -1,11 +1,32 @@
+/* =============================================================================
+   DIWO Vergleich - CSS Stylesheet
+   =============================================================================
+   
+   Inhaltsverzeichnis:
+   1. CSS Custom Properties (Variablen)
+   2. Reset & Base Styles
+   3. Layout Components
+   4. Form Elements
+   5. Button Styles
+   6. Table Styles
+   7. Section Components (Collapsible Sections)
+   8. Specific Table Types
+   9. Legend & Color Picker
+   10. Header Components
+   11. Responsive Design
+   ============================================================================= */
+
+/* =============================================================================
+   1. CSS CUSTOM PROPERTIES (VARIABLEN)
+   ============================================================================= */
 :root {
   /* Standard Farben mit verbessertem Kontrast */
   --rot-hell: #ffe6e6;
   --gelb-hell: #fff8db;
   --gruen-hell: #e8f5e8;
-  --rot: #b30000;      /* Verbessert von #800000 für besseren Kontrast */
-  --gelb: #9d5502;     /* Verbessert von #784801 für besseren Kontrast */
-  --gruen: #004d26;    /* Verbessert von #006633 für besseren Kontrast */
+  --rot: #b30000;
+  --gelb: #9d5502;
+  --gruen: #004d26;
   --weiss: #ffffff;
   --schwarz: #000000;
   --grau-hell: #f7f9fa;
@@ -24,11 +45,14 @@
   --rot-hell: #ffe6f5;
   --gelb-hell: #ffeccc;
   --gruen-hell: #e6f9ff;
-  --rot: #cc0044;      /* Verbessert für besseren Kontrast */
-  --gelb: #b8620e;     /* Verbessert für besseren Kontrast */
-  --gruen: #008a8a;    /* Verbessert für besseren Kontrast */
+  --rot: #cc0044;
+  --gelb: #b8620e;
+  --gruen: #008a8a;
 }
 
+/* =============================================================================
+   2. RESET & BASE STYLES
+   ============================================================================= */
 * {
   box-sizing: border-box;
 }
@@ -38,7 +62,12 @@ body {
   padding: 16px;
   background: var(--grau-hell);
   color: var(--schwarz);
-  font: 14px/1.5 system-ui, sans-serif; /* Verbesserte Zeilenhöhe für bessere Lesbarkeit */
+  font: 14px/1.5 system-ui, sans-serif;
+}
+
+h1, h2, h3 {
+  margin: 0.2rem 0 0.6rem;
+  line-height: 1.3;
 }
 
 /* Focus-Stile für bessere Keyboard-Navigation */
@@ -47,7 +76,6 @@ body {
   outline-offset: 2px;
 }
 
-/* Verbesserter Focus für interaktive Elemente */
 button:focus,
 select:focus,
 input:focus,
@@ -57,15 +85,16 @@ input:focus,
   outline-offset: 2px;
 }
 
-h1, h2, h3 {
-  margin: 0.2rem 0 0.6rem;
-  line-height: 1.3; /* Verbesserte Zeilenhöhe für Überschriften */
-}
-
-.muted {
-  opacity: 0.8;
+tr:focus {
+  outline: var(--focus-outline);
+  outline-offset: 1px;
+  position: relative;
+  z-index: 1;
 }
 
+/* =============================================================================
+   3. LAYOUT COMPONENTS
+   ============================================================================= */
 .row {
   display: flex;
   gap: 0.5rem;
@@ -82,42 +111,24 @@ h1, h2, h3 {
   grid-column: 1/-1;
 }
 
-textarea,
-input,
-select {
-  width: 100%;
-  padding: 0.5rem;
-  background: var(--weiss);
-  color: var(--schwarz);
-  border: 2px solid var(--grau-stark); /* Dickerer Border für bessere Sichtbarkeit */
-  border-radius: 4px;
-  font-size: 14px; /* Explizite Schriftgröße für bessere Lesbarkeit */
-}
-
-/* Hover-Effekte für bessere Interaktivität */
-textarea:hover,
-input:hover,
-select:hover {
-  border-color: var(--diwo-blau);
+.freibetrag-container {
+  display: flex;
+  gap: 16px;
+  margin: 0.6rem 0 1rem;
 }
 
-button {
-  padding: 0.5rem 0.9rem;
-  border: 2px solid var(--grau-stark);
-  background: var(--weiss);
-  cursor: pointer;
-  font-size: 14px;
-  min-height: 44px; 
-  transition: all 0.2s ease;
+.freibetrag-section {
+  flex: 1;
+  min-width: 0;
 }
 
-button:hover {
-  background: var(--grau-hell);
-  border-color: var(--diwo-blau);
+.freibetrag-section h3 {
+  margin: 0 0 0.6rem;
+  font-size: 16px;
 }
 
-button:active {
-  transform: translateY(1px);
+.kinderbetreuung-container {
+  max-width: 50%;
 }
 
 .error-box {
@@ -128,126 +139,25 @@ button:active {
   border-radius: 4px;
 }
 
-/* DIWO Header Styles */
-.diwo-header {
-  background: var(--weiss);
-  border: 1px solid var(--grau-stark);
-  padding: 12px 16px;
-  margin-bottom: 16px;
-  border-radius: 4px;
-}
-
-.header-row-1 {
-  display: flex;
-  align-items: center;
-  gap: 20px;
-  margin-bottom: 8px;
-  padding-bottom: 8px;
-  border-bottom: 1px solid var(--grau-stark);
-}
-
-.kunde-logo {
-  width: auto;
-  height: 50px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.kunde-logo-container {
-  display: flex;
-  align-items: center;
-  justify-content: center;
+/* =============================================================================
+   4. FORM ELEMENTS
+   ============================================================================= */
+textarea,
+input,
+select {
   width: 100%;
-  height: 100%;
-}
-
-.kunde-logo-image {
-  max-width: auto;
-  max-height: 50px;
-  object-fit: contain;
-  border-radius: 4px;
-}
-
-.diwo-title {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-start;
-}
-
-.diwo-title h1 {
-  font-size: 24px;
-  font-weight: bold;
-  margin: 0;
-  color: var(--diwo-blau);
-}
-
-.diwo-title h2 {
-  font-size: 18px;
-  font-weight: normal;
-  margin: 0;
+  padding: 0.5rem;
+  background: var(--weiss);
   color: var(--schwarz);
-}
-
-.page-title {
-  font-size: 16px;
-  font-weight: bold;
-  margin-left: auto;
-}
-
-.header-row-2 {
-  display: grid;
-  grid-template-columns: auto 1fr auto;
-  gap: 20px;
-  margin-bottom: 8px;
-  align-items: center;
-}
-
-.aktenzeichen {
-  font-weight: bold;
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px;
   font-size: 14px;
 }
 
-.antragsteller-info {
-  text-align: center;
-  font-weight: bold;
-}
-
-.location-info {
-  text-align: right;
-  font-size: 13px;
-}
-
-.header-row-3 {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-top: 8px;
-  padding-top: 8px;
-  border-top: 1px solid var(--grau-stark);
-}
-
-.vorgang-info {
-  font-size: 13px;
-}
-
-/* Vergleichssteuerungen */
-.vergleich-controls {
-  display: flex;
-  align-items: center;
-}
-
-.vergleich-controls-wrapper {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-}
-
-.vorgang-form,
-.zeitraum-form {
-  display: flex;
-  align-items: center;
-  gap: 8px;
+textarea:hover,
+input:hover,
+select:hover {
+  border-color: var(--diwo-blau);
 }
 
 .control-group {
@@ -270,66 +180,87 @@ button:active {
   min-height: 32px;
 }
 
-/* Vergleichslegende */
-.vergleich-legende {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  margin: 12px 0;
-  padding: 8px 12px;
-  background: var(--grau-hell);
-  border: 1px solid var(--grau-stark);
-  border-radius: 4px;
+.color-scheme-select {
+  width: 100%;
+  padding: 6px 8px;
   font-size: 13px;
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px;
+  min-height: 32px;
 }
 
-.legende-item {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  padding: 4px 8px;
-  border-radius: 3px;
+/* =============================================================================
+   5. BUTTON STYLES
+   ============================================================================= */
+button {
+  padding: 0.5rem 0.9rem;
+  border: 2px solid var(--grau-stark);
+  background: var(--weiss);
+  cursor: pointer;
+  font-size: 14px;
+  min-height: 44px; 
+  transition: all 0.2s ease;
 }
 
-.legende-item:has(.plus) {
-  background: var(--gruen-hell);
-  border: 1px solid var(--gruen); 
+button:hover {
+  background: var(--grau-hell);
+  border-color: var(--diwo-blau);
 }
 
-.legende-item:has(.minus) {
-  background: var(--rot-hell);
-  border: 1px solid var(--rot);
+button:active {
+  transform: translateY(1px);
 }
 
-.legende-item:has(.warn) {
-  background: var(--gelb-hell);
-  border: 1px solid var(--gelb);
+/* Aufklapp-Button für Personendetails */
+.expand-btn {
+  background: none;
+  border: 1px solid var(--grau-stark);
+  cursor: pointer;
+  font-size: 14px;
+  padding: 4px 6px;
+  border-radius: 3px;
+  transition: all 0.2s ease;
+  min-height: 28px;
+  min-width: 28px;
+  color: var(--diwo-blau);
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
-.legende-title {
-  margin-right: 8px;
-  font-size: 13px;
+.expand-btn:hover {
+  background: var(--diwo-blau-hell);
+  border-color: var(--diwo-blau);
 }
 
-.legende-spacer {
-  color: var(--grau-stark);
+.expand-btn:focus {
+  outline: 2px solid var(--focus-color);
+  outline-offset: 1px;
+}
+
+.expand-btn span {
+  transition: transform 0.2s ease;
   font-weight: bold;
 }
 
-.selected-indicator-text {
-  border: 3px solid var(--diwo-blau);
-  padding: 2px 4px;
-  border-radius: 2px;
-  display: inline-block;
+.expand-btn[aria-expanded="true"] span {
+  transform: rotate(90deg);
+}
+
+.expand-btn:focus-visible {
+  outline: 3px solid var(--focus-color);
+  outline-offset: 2px;
 }
 
+/* =============================================================================
+   6. TABLE STYLES
+   ============================================================================= */
 table {
   width: 100%;
   border-collapse: collapse;
   background: var(--weiss);
   border: 1px solid var(--grau-stark);
   margin: 0.6rem 0 1rem;
-  /* Zusätzlicher Platz für Focus-Outlines innerhalb der Tabelle */
   border-spacing: 0;
 }
 
@@ -347,6 +278,8 @@ th {
   font-weight: bold;
   color: var(--schwarz); 
 }
+
+/* Symbol-Spalte */
 .sym {
   width: 2.5rem;
   min-width: 2.5rem;
@@ -368,6 +301,7 @@ th {
   color: var(--gelb);
 }
 
+/* Diff-Highlighting für Tabellenzeilen */
 .diff-row-added {
   background: var(--gruen-hell);
 }
@@ -381,7 +315,6 @@ td:has(.cell-changed) {
   background: var(--gelb-hell);
 }
 
-/* Verbessertes Icon für geänderte Zellen */
 .cell-changed:not(.sym)::before {
   content: "! ";
   color: var(--gelb);
@@ -398,79 +331,321 @@ td:has(.cell-changed) {
   font-style: italic;
 }
 
-.header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 8px;
+/* Klickbare Tabellenzeilen für Personenauswahl */
+.person-row {
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+  position: relative;
+}
+
+.person-row:hover {
+  background-color: var(--diwo-blau-hell);
 }
 
-.legend {
-  margin: 0.5rem 0 1rem;
+.person-row:focus {
+  background-color: var(--diwo-blau-hell);
+  outline: var(--focus-outline);
+  outline-offset: 1px;
+  z-index: 1;
 }
 
+/* Ausgewählte Person - nur auf HHM-Seite anzeigen */
 .person-selected {
+  outline: none;
+}
+
+.hhm-page .person-selected {
   outline: 3px solid var(--diwo-blau);
   outline-offset: -3px;
 }
 
-/* Clickable table rows mit verbesserter Accessibility */
-.person-row {
+/* =============================================================================
+   7. SECTION COMPONENTS (COLLAPSIBLE SECTIONS)
+   ============================================================================= */
+.section-container {
+  margin: 0.6rem 0 1rem;
+}
+
+.section-header {
+  background: var(--grau-stark);
+  padding: 12px 16px;
+  border: 2px solid var(--grau-stark);
+  border-radius: 4px 4px 0 0;
   cursor: pointer;
-  transition: background-color 0.2s ease;
-  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  user-select: none;
+  transition: all 0.2s ease;
+  min-height: 44px;
 }
 
-.person-row:hover {
-  background-color: var(--diwo-blau-hell);
+.section-header:hover {
+  background: #c5c6c7;
+  border-color: var(--diwo-blau);
 }
 
-.person-row:focus {
-  background-color: var(--diwo-blau-hell);
-  outline: var(--focus-outline);
-  outline-offset: 1px; /* Reduzierter Offset für bessere Sichtbarkeit in Tabellen */
-  z-index: 1; /* Stelle sicher, dass der Focus-Ring über anderen Elementen liegt */
+.section-header h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: bold;
 }
 
-/* Verbesserte Focus-Styles für alle Tabellenzeilen */
-tr:focus {
-  outline: var(--focus-outline);
-  outline-offset: 1px;
-  position: relative;
-  z-index: 1;
+.toggle-icon {
+  font-size: 14px;
+  transition: transform 0.2s ease;
+  font-weight: bold;
+}
+
+.toggle-icon.collapsed {
+  transform: rotate(-90deg);
+}
+
+.section-content {
+  border: 2px solid var(--grau-stark);
+  border-top: none;
+  border-radius: 0 0 4px 4px;
+  transition: max-height 0.3s ease;
+  overflow: visible;
+  padding: 4px;
+}
+
+.section-content.collapsed {
+  display: none;
+}
+
+.section-content table {
+  margin: 0;
+  border: none;
+}
+
+/* =============================================================================
+   8. SPECIFIC TABLE TYPES
+   ============================================================================= */
+
+/* Expandable Tables für Personendetails */
+.expandable-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0.6rem 0 1rem;
+  border-spacing: 0;
+}
+
+.expand-col {
+  width: 40px;
+  min-width: 40px;
+  max-width: 40px;
+  text-align: center;
+  padding: 0.25rem;
+}
+
+/* Detail-Zeilen für Personendetails */
+.person-details-row {
+  transition: all 0.3s ease;
+}
+
+.person-details-row.hidden {
+  display: none;
+}
+
+.details-cell {
+  padding: 16px;
+  background: var(--grau-stark) !important;
+  border-left: 4px solid var(--diwo-blau);
+}
+
+.details-container {
+  max-width: 100%;
+}
+
+.details-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin-top: 8px;
+  font-size: 14px;
+}
+
+.details-table td {
+  border: 1px solid var(--grau-stark);
+  padding: 6px 8px;
+  vertical-align: top;
+  background: var(--weiss);
+}
+
+.detail-label {
+  font-weight: bold;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  width: 12.5%;
+  min-width: 80px;
+}
+
+/* Wohnung Grid Table */
+.wohnung-grid-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0;
+  font-size: 14px;
+}
+
+.wohnung-grid-table td {
+  border: 1px solid var(--grau-stark);
+  padding: 8px 12px;
+  vertical-align: middle;
+}
+
+.label-cell {
+  font-weight: bold;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  white-space: nowrap;
+}
+
+.value-cell {
+  background: var(--weiss);
+}
+
+/* Miete Grid Table */
+.miete-grid-table {
+  width: 100%;
+  border-collapse: collapse;
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  margin: 0;
+  font-size: 14px;
+  table-layout: fixed;
+}
+
+.miete-grid-table td,
+.miete-grid-table th {
+  border: 1px solid var(--grau-stark);
+  padding: 8px;
+  vertical-align: middle;
+  text-align: left;
+}
+
+/* Header-Zellen in Miete-Tabelle */
+.miete-grid-table .miete-header-cell {
+  font-weight: bold;
+  background: var(--grau-stark);
+  color: var(--schwarz);
+  text-align: left;
+}
+
+/* Verschiedene Zelltypen in Miete-Tabelle */
+.miete-grid-table .label-cell {
+  font-weight: bold;
+  background: var(--grau-hell);
+  color: var(--schwarz);
+  white-space: nowrap;
+  width: 15%;
+}
+
+.miete-grid-table .value-cell {
+  background: var(--weiss);
+  text-align: left;
+  width: 15%;
+}
+
+.miete-grid-table .pauschale-cell {
+  background: var(--weiss);
+  text-align: left;
+  font-style: normal;
+  color: var(--schwarz);
+  font-size: 14px;
+  width: 15%;
+}
+
+.miete-grid-table .gewerbefläche-cell {
+  background: var(--weiss);
+  color: var(--schwarz);
+  font-weight: normal;
+  text-align: left;
+}
+
+.miete-grid-table .empty-cell {
+  background: var(--weiss);
+  width: 10%;
+}
+
+.miete-grid-table .section-header-cell {
+  font-weight: bold;
+  color: var(--diwo-blau);
+  background: var(--grau-stark);
+  text-align: left;
+}
+
+/* =============================================================================
+   9. LEGEND & COLOR PICKER
+   ============================================================================= */
+.vergleich-legende {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin: 12px 0;
+  padding: 8px 12px;
+  background: var(--grau-hell);
+  border: 1px solid var(--grau-stark);
+  border-radius: 4px;
+  font-size: 13px;
+}
+
+.legende-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 8px;
+  border-radius: 3px;
+}
+
+.legende-item:has(.plus) {
+  background: var(--gruen-hell);
+  border: 1px solid var(--gruen); 
+}
+
+.legende-item:has(.minus) {
+  background: var(--rot-hell);
+  border: 1px solid var(--rot);
+}
+
+.legende-item:has(.warn) {
+  background: var(--gelb-hell);
+  border: 1px solid var(--gelb);
+}
+
+.legende-title {
+  margin-right: 8px;
+  font-size: 13px;
 }
 
-/* Skip to content für Screen Reader */
-.skip-link {
-  position: absolute;
-  top: -40px;
-  left: 6px;
-  background: var(--diwo-blau);
-  color: var(--weiss);
-  padding: 8px;
-  text-decoration: none;
-  border-radius: 4px;
-  z-index: 1000;
+.legende-spacer {
+  color: var(--grau-stark);
+  font-weight: bold;
 }
 
-.skip-link:focus {
-  top: 6px;
+.selected-indicator-text {
+  border: 3px solid var(--diwo-blau);
+  padding: 2px 4px;
+  border-radius: 2px;
+  display: inline-block;
 }
 
-.table-action {
-  padding: 0.25rem 0.5rem;
-  border: 1px solid var(--grau-stark);
-  background: #fff;
-  cursor: pointer;
-  font-size: 0.85rem;
-  min-height: 32px;
+/* Legende-Item für ausgewählte Person nur auf HHM-Seite anzeigen */
+.hhm-only {
+  display: none;
 }
 
-.table-action:hover {
-  filter: brightness(0.98);
+.hhm-page .hhm-only {
+  display: flex;
 }
 
-/* Farbwahl in der Legende */
+/* Color Picker */
 .color-picker-container {
   position: relative;
   display: flex;
@@ -520,225 +695,181 @@ tr:focus {
   display: block;
 }
 
-.color-scheme-select {
-  width: 100%;
-  padding: 6px 8px;
-  font-size: 13px;
-  border: 2px solid var(--grau-stark);
+/* =============================================================================
+   10. HEADER COMPONENTS
+   ============================================================================= */
+.diwo-header {
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  padding: 12px 16px;
+  margin-bottom: 16px;
   border-radius: 4px;
-  min-height: 32px;
 }
 
-/* Responsive Layout für Freibeträge und Kinderbetreuung */
-.freibetrag-container {
+.header-row-1 {
   display: flex;
-  gap: 16px;
-  margin: 0.6rem 0 1rem;
-}
-
-.freibetrag-section {
-  flex: 1;
-  min-width: 0;
-}
-
-.freibetrag-section h3 {
-  margin: 0 0 0.6rem;
-  font-size: 16px;
-}
-
-.kinderbetreuung-container {
-  max-width: 50%;
-}
-
-/* Bei kleinen Bildschirmen: Untereinander anordnen */
-@media (max-width: 1200px) {
-  .freibetrag-container {
-    flex-direction: column;
-  }
-
-  .kinderbetreuung-container {
-    max-width: 100%;
-  }
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 8px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid var(--grau-stark);
 }
 
-/* Ausklappbare Sektionen */
-.section-container {
-  margin: 0.6rem 0 1rem;
+.kunde-logo {
+  width: auto;
+  height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
 }
 
-.section-header {
-  background: var(--grau-stark);
-  padding: 12px 16px;
-  border: 2px solid var(--grau-stark);
-  border-radius: 4px 4px 0 0;
-  cursor: pointer;
+.kunde-logo-container {
   display: flex;
   align-items: center;
-  gap: 8px;
-  user-select: none;
-  transition: all 0.2s ease;
-  min-height: 44px;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
 }
 
-.section-header:hover {
-  background: #c5c6c7;
-  border-color: var(--diwo-blau);
+.kunde-logo-image {
+  max-width: auto;
+  max-height: 50px;
+  object-fit: contain;
+  border-radius: 4px;
 }
 
-
-.section-header h3 {
-  margin: 0;
-  font-size: 16px;
-  font-weight: bold;
+.diwo-title {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
 }
 
-.toggle-icon {
-  font-size: 14px;
-  transition: transform 0.2s ease;
+.diwo-title h1 {
+  font-size: 24px;
   font-weight: bold;
-}
-
-.toggle-icon.collapsed {
-  transform: rotate(-90deg);
-}
-
-.section-content {
-  border: 2px solid var(--grau-stark);
-  border-top: none;
-  border-radius: 0 0 4px 4px;
-  transition: max-height 0.3s ease;
-  overflow: visible; /* Geändert von hidden zu visible für Focus-Outlines */
-  padding: 4px; /* Zusätzlicher Platz für Focus-Outlines */
-}
-
-.section-content.collapsed {
-  display: none;
-}
-
-.section-content table {
   margin: 0;
-  border: none;
+  color: var(--diwo-blau);
 }
 
-/* ===== Erweiterte Stile für aufklappbare Personendetails ===== */
-
-/* Expandable table styles */
-.expandable-table {
-  width: 100%;
-  border-collapse: collapse;
-  background: var(--weiss);
-  border: 1px solid var(--grau-stark);
-  margin: 0.6rem 0 1rem;
-  border-spacing: 0;
+.diwo-title h2 {
+  font-size: 18px;
+  font-weight: normal;
+  margin: 0;
+  color: var(--schwarz);
 }
 
-/* Expand column für den Aufklapp-Button */
-.expand-col {
-  width: 40px;
-  min-width: 40px;
-  max-width: 40px;
+.page-title {
+  font-size: 16px;
+  font-weight: bold;
+  flex: 1;
   text-align: center;
-  padding: 0.25rem;
 }
 
-/* Aufklapp-Button Stile */
-.expand-btn {
-  background: none;
-  border: 1px solid var(--grau-stark);
-  cursor: pointer;
-  font-size: 14px;
-  padding: 4px 6px;
-  border-radius: 3px;
-  transition: all 0.2s ease;
-  min-height: 28px;
-  min-width: 28px;
-  color: var(--diwo-blau);
-  display: flex;
+.header-row-2 {
+  display: grid;
+  grid-template-columns: auto 1fr auto;
+  gap: 20px;
+  margin-bottom: 8px;
   align-items: center;
-  justify-content: center;
-}
-
-.expand-btn:hover {
-  background: var(--diwo-blau-hell);
-  border-color: var(--diwo-blau);
 }
 
-.expand-btn:focus {
-  outline: 2px solid var(--focus-color);
-  outline-offset: 1px;
+.aktenzeichen {
+  font-weight: bold;
+  font-size: 14px;
 }
 
-.expand-btn span {
-  transition: transform 0.2s ease;
+.antragsteller-info {
+  text-align: center;
   font-weight: bold;
 }
 
-.expand-btn[aria-expanded="true"] span {
-  transform: rotate(90deg);
+.location-info {
+  text-align: right;
+  font-size: 13px;
 }
 
-/* Detail-Zeilen Stile */
-.person-details-row {
-  transition: all 0.3s ease;
+.header-row-3 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 8px;
+  padding-top: 8px;
+  border-top: 1px solid var(--grau-stark);
 }
 
-.person-details-row.hidden {
-  display: none;
+.vorgang-info {
+  font-size: 13px;
 }
 
-.details-cell {
-  padding: 16px;
-  background: var(--grau-stark) !important;
-  border-left: 4px solid var(--diwo-blau);
+/* Vergleichssteuerungen */
+.vergleich-controls {
+  display: flex;
+  align-items: center;
 }
 
-.details-container {
-  max-width: 100%;
+.vergleich-controls-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 16px;
 }
 
-/* Details-Tabelle Styling */
-.details-table {
-  width: 100%;
-  border-collapse: collapse;
-  background: var(--weiss);
-  border: 1px solid var(--grau-stark);
-  margin-top: 8px;
-  font-size: 14px;
+.vorgang-form,
+.zeitraum-form {
+  display: flex;
+  align-items: center;
+  gap: 8px;
 }
 
-.details-table td {
-  border: 1px solid var(--grau-stark);
-  padding: 6px 8px;
-  vertical-align: top;
-  background: var(--weiss);
-}
+/* =============================================================================
+   11. RESPONSIVE DESIGN
+   ============================================================================= */
 
-.detail-label {
-  font-weight: bold;
-  background: var(--grau-hell);
-  color: var(--schwarz);
-  width: 12.5%;
-  min-width: 80px;
-}
+/* Bei kleinen Bildschirmen: Freibeträge untereinander anordnen */
+@media (max-width: 1200px) {
+  .freibetrag-container {
+    flex-direction: column;
+  }
 
+  .kinderbetreuung-container {
+    max-width: 100%;
+  }
+}
 
-/* Responsive Anpassungen für Details-Tabelle */
-@media (max-width: 768px) {
-  .details-table {
+/* Responsive Verhalten für Miete-Grid */
+@media (max-width: 1400px) {
+  .miete-grid-table {
+    table-layout: auto;
     font-size: 13px;
   }
   
-  .details-table td {
-    padding: 4px 6px;
+  .miete-grid-table td,
+  .miete-grid-table th {
+    padding: 6px;
   }
   
-  .detail-label {
-    min-width: 60px;
+  .miete-grid-table .label-cell,
+  .miete-grid-table .value-cell,
+  .miete-grid-table .pauschale-cell,
+  .miete-grid-table .empty-cell {
+    width: auto;
   }
 }
 
-/* Verbesserte Keyboard-Navigation für aufklappbare Bereiche */
-.expand-btn:focus-visible {
-  outline: 3px solid var(--focus-color);
-  outline-offset: 2px;
+@media (max-width: 1000px) {
+  .miete-grid-table {
+    font-size: 12px;
+  }
+  
+  .miete-grid-table td,
+  .miete-grid-table th {
+    padding: 4px;
+  }
+}
+
+@media (max-width: 800px) {
+  .miete-grid-table {
+    display: block;
+    overflow-x: auto;
+    white-space: nowrap;
+  }
 }

+ 1 - 1
src/main/resources/templates/fragments/header.html

@@ -13,7 +13,7 @@
         <h1>DIWO</h1>
         <h2>Vergleich</h2>
       </div>
-      <div class="page-title" role="heading" aria-level="2">Haushaltsmitglieder/Einkommen</div>
+      <div class="page-title" role="heading" aria-level="2" th:text="${pageTitle ?: 'Vergleich'}">Vergleich</div>
     </div>
     
     <div class="header-row-2">

+ 1 - 1
src/main/resources/templates/fragments/legend.html

@@ -18,7 +18,7 @@
       <span>geändert (<span id="changedColorName">gelb</span>)</span>
     </div>
     <div class="legende-spacer" aria-hidden="true">|</div>
-    <div class="legende-item">
+    <div class="legende-item hhm-only" style="display: none;">
       <span class="selected-indicator-text">ausgewählte Person (blauer Rahmen)</span>
     </div>
     

+ 5 - 2
src/main/resources/templates/hhm-einkommen.html

@@ -4,7 +4,10 @@
     <meta charset="UTF-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <title>DIWO Vergleich – Haushaltsmitglieder/Einkommen</title>
-    <link rel="stylesheet" href="/styles.css"/>
+    <link rel="stylesheet" href="/css/global.css"/>
+    <link rel="stylesheet" href="/css/header.css"/>
+    <link rel="stylesheet" href="/css/legend.css"/>
+    <link rel="stylesheet" href="/css/hhm-einkommen.css"/>
     <!-- JavaScript Module -->
     <script src="/js/cookie-utils.js"></script>
     <script src="/js/ajax-handlers.js"></script>
@@ -13,7 +16,7 @@
     <script src="/js/color-scheme.js"></script>
     <script src="/js/app.js"></script>
 </head>
-<body>
+<body class="hhm-page">
 
   <!-- Header-Fragment einbinden -->
   <div th:replace="~{fragments/header :: header}"></div>

+ 237 - 30
src/main/resources/templates/miete.html

@@ -4,7 +4,10 @@
     <meta charset="UTF-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
     <title>DIWO Vergleich – Miete</title>
-    <link rel="stylesheet" href="/styles.css"/>
+    <link rel="stylesheet" href="/css/global.css"/>
+    <link rel="stylesheet" href="/css/header.css"/>
+    <link rel="stylesheet" href="/css/legend.css"/>
+    <link rel="stylesheet" href="/css/miete.css"/>
     <!-- JavaScript Module -->
     <script src="/js/cookie-utils.js"></script>
     <script src="/js/ajax-handlers.js"></script>
@@ -21,46 +24,250 @@
   <!-- Vergleichslegende-Fragment einbinden -->
   <div th:replace="~{fragments/legend :: legend}"></div>
 
-  <!-- Miete Hauptinhalt -->
+  <!-- Wohnung Header-Vergleich -->
   <main role="main">
+    <div class="section-container">
+      <div class="section-header" onclick="toggleSection('wohnung')" role="button" tabindex="0" 
+           onkeypress="handleKeyPress(event, 'wohnung')" aria-expanded="true" aria-controls="wohnung-content">
+        <span class="toggle-icon" id="wohnung-icon" aria-hidden="true">▼</span>
+        <h3>Wohnung</h3>
+      </div>
+      <div class="section-content" id="wohnung-content">
+        <div th:if="${wohnungZeile != null}" th:class="${wohnungZeile.rowClass}">
+          <!-- Wohnung Grid als Tabelle -->
+          <table class="wohnung-grid-table">
+            <tr>
+              <td class="label-cell">Mietverhältnis</td>
+              <td class="value-cell" th:utext="${wohnungZeile.mietverhaeltnisHtml}"></td>
+              <td class="label-cell">Fläche (m²)</td>
+              <td class="value-cell" th:utext="${wohnungZeile.flaecheHtml}"></td>
+              <td class="label-cell">Bad</td>
+              <td class="value-cell" th:utext="${wohnungZeile.badHtml}"></td>
+              <td class="label-cell">Name (Vermiet)</td>
+              <td class="value-cell" th:utext="${wohnungZeile.nameEigHtml}"></td>
+              <td class="label-cell">Telefon</td>
+              <td class="value-cell" th:utext="${wohnungZeile.telefonEigHtml}"></td>
+            </tr>
+            <tr>
+              <td class="label-cell">öffentl. geförd. Wohnungsbau</td>
+              <td class="value-cell" th:utext="${wohnungZeile.sozBauJnHtml}"></td>
+              <td class="label-cell">Bezugsfert</td>
+              <td class="value-cell" th:utext="${wohnungZeile.fertigDatHtml}"></td>
+              <td class="label-cell">Heizung</td>
+              <td class="value-cell" th:utext="${wohnungZeile.heizungHtml}"></td>
+              <td class="label-cell">Adresse</td>
+              <td class="value-cell" th:utext="${wohnungZeile.adresseEigHtml}"></td>
+              <td colspan="2"></td> <!-- Leere Zellen für das 5. Paar -->
+            </tr>
+          </table>
+        </div>
+      </div>
+    </div>
+
+    <!-- Miete -->
     <div class="section-container">
       <div class="section-header" onclick="toggleSection('miete')" role="button" tabindex="0" 
            onkeypress="handleKeyPress(event, 'miete')" aria-expanded="true" aria-controls="miete-content">
         <span class="toggle-icon" id="miete-icon" aria-hidden="true">▼</span>
-        <h3>Mietdaten</h3>
+        <h3>Miete</h3>
       </div>
       <div class="section-content" id="miete-content">
-        <!-- Hier werden später die Mietdaten angezeigt -->
-        <p>Mietvergleich wird hier angezeigt.</p>
-        
-        <!-- Debug-Information für JSON-Daten -->
-        <div th:if="${leftJson != null}" style="margin-top: 20px; padding: 10px; background-color: #f5f5f5; border-radius: 5px;">
-          <h4>Erhaltene Daten:</h4>
-          <p><strong>Left JSON erhalten:</strong> <span th:text="${#strings.length(leftJson)}"></span> Zeichen</p>
-          <p><strong>Right JSON erhalten:</strong> <span th:text="${#strings.length(rightJson)}"></span> Zeichen</p>
-          <p><strong>Schema:</strong> <span th:text="${schemaName}"></span></p>
-          <p><strong>Validation:</strong> <span th:text="${validate}"></span></p>
-          <p><strong>Aktiver Kunde:</strong> <span th:text="${activeKunde}"></span></p>
-          
-          <div th:if="${leftCandidates != null and !leftCandidates.empty}">
-            <p><strong>Left Candidates:</strong> <span th:text="${#lists.size(leftCandidates)}"></span></p>
-          </div>
-          
-          <div th:if="${zeitraeumeRight != null and !zeitraeumeRight.empty}">
-            <p><strong>Right Zeiträume:</strong> <span th:text="${#lists.size(zeitraeumeRight)}"></span></p>
-          </div>
-          
-          <div th:if="${stammvorgangRight != null}">
-            <p><strong>Stammvorgang Right ID:</strong> <span th:text="${stammvorgangRight.id()}"></span></p>
-          </div>
-          
-          <div th:if="${stammakteRight != null}">
-            <p><strong>Stammakte Right:</strong> <span th:text="${stammakteRight.aktenzeichen()}"></span></p>
-          </div>
+        <div th:if="${mieteZeile != null}" th:class="${mieteZeile.rowClass}">
+          <!-- Miete Grid als echte Tabelle mit 7 Spalten -->
+          <table class="miete-grid-table">
+            <!-- Header-Zeile mit Spaltenüberschriften -->
+            <thead>
+              <tr>
+                <th class="miete-header-cell" colspan="2">Allgemeine Daten</th>
+                <th class="miete-header-cell" colspan="">Abzüge</th>
+                <th class="miete-header-cell" colspan="">Werte</th>
+                <th class="miete-header-cell" aria-label="Pauschale für Abzugsarten">Pauschale</th>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+            </thead>
+            <tbody>
+              <!-- Erste Zeile: Gesamtmiete -->
+              <tr>
+                <td class="label-cell">* Gesamtmiete</td>
+                <td class="value-cell" th:utext="${mieteZeile.gesamtmieteHtml}"></td>
+                <td class="label-cell">Heizung</td>
+                <td class="value-cell" th:utext="${mieteZeile.heizungHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.heizungPauschaleHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Zweite Zeile: Zuschläge -->
+              <tr>
+                <td class="label-cell">Zuschläge</td>
+                <td class="value-cell" th:utext="${mieteZeile.zuschlägeHtml}"></td>
+                <td class="label-cell">Warmwasser</td>
+                <td class="value-cell" th:utext="${mieteZeile.warmwasserHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.warmwasserPauschaleHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Dritte Zeile: Art des Zuschlages -->
+              <tr>
+                <td class="label-cell">Art des Zuschlages</td>
+                <td class="value-cell" th:utext="${mieteZeile.artZuschlagHtml}"></td>
+                <td class="label-cell">Kosten HH-Energie</td>
+                <td class="value-cell" th:utext="${mieteZeile.kostenHhEnergieHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.energiePauschaleHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Jährliche Kosten Überschrift -->
+              <tr>
+                <td class="section-header-cell" colspan="2">Jährliche Kosten</td>
+                <td class="label-cell">Garage/Stellplatz</td>
+                <td class="value-cell" th:utext="${mieteZeile.stellplatzHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.stellplatzPauschaleHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Fernwärme -->
+              <tr>
+                <td class="label-cell">Fernwärme</td>
+                <td class="value-cell" th:utext="${mieteZeile.jhrlFernwaermeHtml}"></td>
+                <td class="label-cell">Möblierungszuschläge</td>
+                <td class="value-cell" th:utext="${mieteZeile.moeblierungszuschlagHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.moeblierungPauschaleHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Fernwarmwasser -->
+              <tr>
+                <td class="label-cell">Fernwarmwasser</td>
+                <td class="value-cell" th:utext="${mieteZeile.jhrlFernwarmwasserHtml}"></td>
+                <td class="label-cell">Gewerbezuschläge</td>
+                <td class="value-cell" th:utext="${mieteZeile.gewerbezuschlagHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.gewerbePauschaleHtml}"></td>
+                <td class="label-cell">Gewerbefläche in m²</td>
+                <td class="value-cell gewerbefläche-cell" th:utext="${mieteZeile.gewerbeflaecheHtml}"></td>
+              </tr>
+              <!-- Fernwärme und -warmwasser -->
+              <tr>
+                <td class="label-cell">Fernwärme und -warmwasser</td>
+                <td class="value-cell" th:utext="${mieteZeile.jhrlFernwUndWHtml}"></td>
+                <td class="label-cell">Untermietzuschläge</td>
+                <td class="value-cell" th:utext="${mieteZeile.untermietzuschlagHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Sonstige Zuschläge -->
+              <tr>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+                <td class="label-cell">sonstige Zuschläge</td>
+                <td class="value-cell" th:utext="${mieteZeile.sonstZuschlagHtml}"></td>
+                <td class="pauschale-cell" th:utext="${mieteZeile.sonstPauschaleHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+              <!-- Leistungen Dritter -->
+              <tr>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+                <td class="label-cell">Leistungen Dritter</td>
+                <td class="value-cell" th:utext="${mieteZeile.leistungenDritterHtml}"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+                <td class="empty-cell"></td>
+              </tr>
+            </tbody>
+          </table>
         </div>
       </div>
     </div>
 
+    <!-- Untermieter -->
+    <div class="section-container">
+      <div class="section-header" onclick="toggleSection('untermieter')" role="button" tabindex="0" 
+           onkeypress="handleKeyPress(event, 'untermieter')" aria-expanded="true" aria-controls="untermieter-content">
+        <span class="toggle-icon" id="untermieter-icon" aria-hidden="true">▼</span>
+        <h3>Untermieterin/Untermieter</h3>
+      </div>
+      <div class="section-content" id="untermieter-content">
+        <table role="table" aria-labelledby="untermieter-caption" id="untermieter-table">
+          <thead>
+            <tr>
+              <th scope="col">Änderung</th>
+              <th scope="col">Name</th>
+              <th scope="col">Vorname</th>
+              <th scope="col">Miete</th>
+              <th scope="col">Fläche</th>
+              <th scope="col">sonst. Kosten</th>
+              <th scope="col">Heizung</th>
+              <th scope="col">Heizungspauschale</th>
+              <th scope="col">Warmwasser</th>
+              <th scope="col">Warmwasserpauschale</th>
+              <th scope="col">Möblierung</th>
+              <th scope="col">Möblierungspauschale</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="z : ${untermieterZeilen}" th:class="${z.rowClass}">
+              <td class="sym" th:classappend="' ' + ${z.symbolClass}" th:text="${z.symbol}"
+                  th:aria-label="${z.symbol == '+' ? 'Hinzugefügt' : (z.symbol == '-' ? 'Gelöscht' : (z.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
+              <td th:utext="${z.nameHtml}"></td>
+              <td th:utext="${z.vornameHtml}"></td>
+              <td th:utext="${z.mieteHtml}"></td>
+              <td th:utext="${z.flaecheHtml}"></td>
+              <td th:utext="${z.sonstHtml}"></td>
+              <td th:utext="${z.heizungHtml}"></td>
+              <td th:utext="${z.heizungPHtml}"></td>
+              <td th:utext="${z.warmwasserHtml}"></td>
+              <td th:utext="${z.warmwasserPHtml}"></td>
+              <td th:utext="${z.moeblierungHtml}"></td>
+              <td th:utext="${z.moeblierungPHtml}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+
+    <!-- Mitbewohner -->
+    <div class="section-container">
+      <div class="section-header" onclick="toggleSection('mitbewohner')" role="button" tabindex="0" 
+           onkeypress="handleKeyPress(event, 'mitbewohner')" aria-expanded="true" aria-controls="mitbewohner-content">
+        <span class="toggle-icon" id="mitbewohner-icon" aria-hidden="true">▼</span>
+        <h3>mitbewohnende Person</h3>
+      </div>
+      <div class="section-content" id="mitbewohner-content">
+        <table role="table" aria-labelledby="mitbewohner-caption" id="mitbewohner-table">
+          <thead>
+            <tr>
+              <th scope="col">Änderung</th>
+              <th scope="col">Name</th>
+              <th scope="col">Vorname</th>
+              <th scope="col">Miete</th>
+              <th scope="col">sonst. Kosten</th>
+              <th scope="col">Heizung</th>
+              <th scope="col">Heizungspauschale</th>
+              <th scope="col">Warmwasser</th>
+              <th scope="col">Warmwasserpauschale</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="z : ${mitbewohnerZeilen}" th:class="${z.rowClass}">
+              <td class="sym" th:classappend="' ' + ${z.symbolClass}" th:text="${z.symbol}"
+                  th:aria-label="${z.symbol == '+' ? 'Hinzugefügt' : (z.symbol == '-' ? 'Gelöscht' : (z.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
+              <td th:utext="${z.nameHtml}"></td>
+              <td th:utext="${z.vornameHtml}"></td>
+              <td th:utext="${z.entgeltHtml}"></td>
+              <td th:utext="${z.sonstHtml}"></td>
+              <td th:utext="${z.heizungHtml}"></td>
+              <td th:utext="${z.heizungPHtml}"></td>
+              <td th:utext="${z.warmwasserHtml}"></td>
+              <td th:utext="${z.warmwasserPHtml}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+
     <!-- Hidden data for AJAX requests -->
     <div id="ajax-data" style="display: none;" aria-hidden="true">
       <input type="hidden" id="leftJson" th:value="${leftJson}"/>

+ 2 - 2
src/main/resources/templates/test.html

@@ -1,6 +1,6 @@
 <!DOCTYPE html>
 <html lang="de" xmlns:th="http://www.thymeleaf.org">
-<head><meta charset="UTF-8"/><title>Vergleich – Test</title><link rel="stylesheet" href="/styles.css"/></head>
+<head><meta charset="UTF-8"/><title>Vergleich – Test</title><link rel="stylesheet" href="/css/global.css"/><link rel="stylesheet" href="/css/header.css"/><link rel="stylesheet" href="/css/legend.css"/></head>
 <body>
   <h1>Vergleich – Testseite</h1>
   
@@ -25,7 +25,7 @@
     <div><label>Right JSON (Neu/Wahrheit)</label><textarea id="rightJson" name="rightJson" th:text="${rightJson}" rows="20"></textarea></div>
     <div class="full row">
       <label><input type="checkbox" name="validate" th:checked="${validate ?: true}"> Gegen Schema validieren</label>
-      <label>Schema-Name: <input type="text" id="schemaNameInput" name="schemaName" th:value="${schemaName ?: 'haushalt-einkommen'}"></label>
+      <label>Schema-Name: <input disabled type="text" id="schemaNameInput" name="schemaName" th:value="${schemaName ?: 'haushalt-einkommen'}"></label>
       <label>Kunde/Bundesland: 
         <select name="kunde">
           <option value="">-- Kein Logo --</option>

+ 0 - 278
src/main/resources/templates/vergleich.html

@@ -1,278 +0,0 @@
-<!DOCTYPE html>
-<html lang="de" xmlns:th="http://www.thymeleaf.org">
-<head>
-    <meta charset="UTF-8"/>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
-    <title>DIWO Vergleich – Haushaltsmitglieder/Einkommen</title>
-    <link rel="stylesheet" href="/styles.css"/>
-    <!-- JavaScript Module -->
-    <script src="/js/cookie-utils.js"></script>
-    <script src="/js/ajax-handlers.js"></script>
-    <script src="/js/ui-updater.js"></script>
-    <script src="/js/interactions.js"></script>
-    <script src="/js/color-scheme.js"></script>
-    <script src="/js/app.js"></script>
-</head>
-<body>
-
-  <!-- Header-Fragment einbinden -->
-  <div th:replace="~{fragments/header :: header}"></div>
-
-  <!-- Vergleichslegende-Fragment einbinden -->
-  <div th:replace="~{fragments/legend :: legend}"></div>
-
-  <!-- Haushaltsmitglieder - erweiterte Tabelle mit aufklappbaren Zeilen -->
-  <main role="main">
-    <div class="section-container">
-      <div class="section-header" onclick="toggleSection('haushaltsmitglieder')" role="button" tabindex="0" 
-           onkeypress="handleKeyPress(event, 'haushaltsmitglieder')" aria-expanded="true" aria-controls="haushaltsmitglieder-content">
-        <span class="toggle-icon" id="haushaltsmitglieder-icon" aria-hidden="true">▼</span>
-        <h3>Haushaltsmitglieder</h3>
-      </div>
-      <div class="section-content" id="haushaltsmitglieder-content">
-        <table role="table" aria-labelledby="haushaltsmitglieder-caption" class="expandable-table" id="haushaltsmitglieder-table">
-          <thead>
-            <tr>
-              <th scope="col" class="expand-col"></th>
-              <th scope="col">Änderung</th>
-              <th scope="col">Name</th>
-              <th scope="col">Blockname</th>
-              <th scope="col">Geburtsdatum</th>
-              <th scope="col">Sterbedatum</th>
-              <th scope="col">Familienstand</th>
-              <th scope="col">Geschlecht</th>
-              <th scope="col">Anrede</th>
-              <th scope="col">Verwandtschaft</th>
-              <th scope="col">Ausschlussgrund</th>
-              <th scope="col">Soz. Stellung</th>
-              <th scope="col">Antragsteller</th>
-              <th scope="col">Zahlungsempfänger</th>
-              <th scope="col">Ende Aufenthaltstitel</th>
-            </tr>
-          </thead>
-          <tbody>
-            <th:block th:each="r : ${mitgliederRows}">
-              <!-- Hauptzeile -->
-              <tr th:class="'person-row ' + ${r.rowClass} + ((${r.personId} == ${personId}) ? ' person-selected' : '')"
-                  th:data-person-id="${r.personId}"
-                  role="button"
-                  tabindex="0"
-                  th:aria-label="'Person auswählen: ' + ${r.nameHtml}"
-                  onkeypress="handleRowKeyPress(event, this)">
-                <td class="expand-col">
-                  <button type="button" 
-                          class="expand-btn" 
-                          th:data-person-id="${r.personId}"
-                          onclick="togglePersonDetailsFromButton(this)"
-                          onkeypress="handleExpandKeyPressFromButton(event, this)"
-                          aria-expanded="false"
-                          th:aria-controls="'person-details-' + ${r.personId}"
-                          th:aria-label="'Details für ' + ${r.nameHtml} + ' anzeigen'">
-                    <span aria-hidden="true">▶</span>
-                  </button>
-                </td>
-                <td class="sym" th:classappend="' ' + ${r.symbolClass}" th:text="${r.symbol}" 
-                    th:aria-label="${r.symbol == '+' ? 'Hinzugefügt' : (r.symbol == '-' ? 'Gelöscht' : (r.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
-                <td th:utext="${r.nameHtml}"></td>
-                <td th:utext="${r.blocknameHtml}"></td>
-                <td th:utext="${r.geburtsdatumHtml}"></td>
-                <td th:utext="${r.sterbedatumHtml}"></td>
-                <td th:utext="${r.familienstandHtml}"></td>
-                <td th:utext="${r.geschlechtHtml}"></td>
-                <td th:utext="${r.anredeHtml}"></td>
-                <td th:utext="${r.verwandtschaftHtml}"></td>
-                <td th:utext="${r.ausschlussgrundHtml}"></td>
-                <td th:utext="${r.sozialstellungHtml}"></td>
-                <td th:utext="${r.antragstellerHtml}"></td>
-                <td th:utext="${r.zahlungsempfaengerHtml}"></td>
-                <td th:utext="${r.aufenthaltstitelEndeHtml}"></td>
-              </tr>
-              
-              <!-- Aufklappbare Detailzeile -->
-              <tr th:id="'person-details-' + ${r.personId}" 
-                  class="person-details-row hidden"
-                  th:aria-labelledby="'person-details-label-' + ${r.personId}">
-                <td colspan="15" class="details-cell">
-                  <div class="details-container">
-                    <table class="details-table">
-                      <tbody>
-                        <tr>
-                          <td class="detail-label">Titel:</td>
-                          <td th:utext="${r.titelHtml}"></td>
-                          <td class="detail-label">Geburtsort:</td>
-                          <td th:utext="${r.geburtsortHtml}"></td>
-                          <td class="detail-label">Telefon:</td>
-                          <td th:utext="${r.telefonHtml}"></td>
-                          <td class="detail-label">Aktenzeichen Soz.:</td>
-                          <td th:utext="${r.aktenzeichenSozialHtml}"></td>
-                        </tr>
-                        <tr>
-                          <td class="detail-label">Geburtsname:</td>
-                          <td th:utext="${r.geburtsnameHtml}"></td>
-                          <td class="detail-label">Alter Name:</td>
-                          <td th:utext="${r.alterNameHtml}"></td>
-                          <td class="detail-label">E-Mail:</td>
-                          <td th:utext="${r.emailHtml}"></td>
-                          <td class="detail-label">Postabrutionsnummer:</td>
-                          <td th:utext="${r.postabrutionsnummerHtml}"></td>
-                        </tr>
-                      </tbody>
-                    </table>
-                  </div>
-                </td>
-              </tr>
-            </th:block>
-          </tbody>
-        </table>
-      </div>
-    </div>
-
-    <!-- Hidden data for AJAX requests -->
-    <div id="ajax-data" style="display: none;" aria-hidden="true">
-      <input type="hidden" id="leftJson" th:value="${leftJson}"/>
-      <input type="hidden" id="rightJson" th:value="${rightJson}"/>
-      <input type="hidden" id="leftVorgangId" th:value="${leftVorgangId}"/>
-      <input type="hidden" id="leftZeitraumId" th:value="${leftZeitraumId}"/>
-      <input type="hidden" id="rightZeitraumId" th:value="${rightZeitraumId}"/>
-      <input type="hidden" id="currentPersonId" th:value="${personId}"/>
-    </div>
-
-    <h2 id="person-details-title">Details zu <span th:if="${personLabel != null and !#strings.isEmpty(personLabel) and personLabel != '(keine Person)'}"><span th:text="${personLabel}"> - </span></span></h2>
-    
-    <!-- Einkommen -->
-    <div class="section-container">
-      <div class="section-header" onclick="toggleSection('einkommen')" role="button" tabindex="0" 
-           onkeypress="handleKeyPress(event, 'einkommen')" aria-expanded="true" aria-controls="einkommen-content">
-        <span class="toggle-icon" id="einkommen-icon" aria-hidden="true">▼</span>
-        <h3>Einkommen</h3>
-      </div>
-      <div class="section-content" id="einkommen-content">
-        <table role="table" aria-labelledby="einkommen-caption" id="einkommen-table">
-          <thead>
-            <tr>
-              <th scope="col">Änderung</th>
-              <th scope="col">Art</th>
-              <th scope="col">Ergänzung</th>
-              <th scope="col">Faktor</th>
-              <th scope="col">Betrag</th>
-              <th scope="col">Zuschlag</th>
-              <th scope="col">Abschlag</th>
-              <th scope="col">Werbungskosten</th>
-            </tr>
-          </thead>
-          <tbody>
-            <tr th:each="z : ${einkommenZeilen}" th:class="${z.rowClass}">
-              <td class="sym" th:classappend="' ' + ${z.symbolClass}" th:text="${z.symbol}"
-                  th:aria-label="${z.symbol == '+' ? 'Hinzugefügt' : (z.symbol == '-' ? 'Gelöscht' : (z.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
-              <td th:utext="${z.artHtml}"></td>
-              <td th:utext="${z.artZusatzHtml}"></td>
-              <td th:utext="${z.faktorHtml}"></td>
-              <td th:utext="${z.betragHtml}"></td>
-              <td th:utext="${z.zuschlagHtml}"></td>
-              <td th:utext="${z.abschlagHtml}"></td>
-              <td th:utext="${z.werbungskostenHtml}"></td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-    </div>
-
-    <div class="freibetrag-container">
-      <div class="freibetrag-section">
-        <div class="section-container">
-          <div class="section-header" onclick="toggleSection('pauschale')" role="button" tabindex="0" 
-               onkeypress="handleKeyPress(event, 'pauschale')" aria-expanded="true" aria-controls="pauschale-content">
-            <span class="toggle-icon" id="pauschale-icon" aria-hidden="true">▼</span>
-            <h3>Pauschale Abzüge</h3>
-          </div>
-          <div class="section-content" id="pauschale-content">
-            <table role="table" aria-labelledby="pauschale-caption" id="pauschale-table">
-              <thead>
-                <tr>
-                  <th scope="col">Änderung</th>
-                  <th scope="col">Art</th>
-                  <th scope="col">Faktor</th>
-                  <th scope="col">Betrag</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr th:each="z : ${freibetragZeilen}" th:if="${z.typ == 'P'}" th:class="${z.rowClass}">
-                  <td class="sym" th:classappend="' ' + ${z.symbolClass}" th:text="${z.symbol}"
-                      th:aria-label="${z.symbol == '+' ? 'Hinzugefügt' : (z.symbol == '-' ? 'Gelöscht' : (z.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
-                  <td th:utext="${z.artHtml}"></td>
-                  <td th:utext="${z.faktorHtml}"></td>
-                  <td th:utext="${z.betragHtml}"></td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-      
-      <div class="freibetrag-section">
-        <div class="section-container">
-          <div class="section-header" onclick="toggleSection('freibetraege')" role="button" tabindex="0" 
-               onkeypress="handleKeyPress(event, 'freibetraege')" aria-expanded="true" aria-controls="freibetraege-content">
-            <span class="toggle-icon" id="freibetraege-icon" aria-hidden="true">▼</span>
-            <h3>Freibeträge</h3>
-          </div>
-          <div class="section-content" id="freibetraege-content">
-            <table role="table" aria-labelledby="freibetraege-caption" id="freibetraege-table">
-              <thead>
-                <tr>
-                  <th scope="col">Änderung</th>
-                  <th scope="col">Art</th>
-                  <th scope="col">Faktor</th>
-                  <th scope="col">Betrag</th>
-                  <th scope="col">Frist</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr th:each="z : ${freibetragZeilen}" th:if="${z.typ == 'F'}" th:class="${z.rowClass}">
-                  <td class="sym" th:classappend="' ' + ${z.symbolClass}" th:text="${z.symbol}"
-                      th:aria-label="${z.symbol == '+' ? 'Hinzugefügt' : (z.symbol == '-' ? 'Gelöscht' : (z.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
-                  <td th:utext="${z.artHtml}"></td>
-                  <td th:utext="${z.faktorHtml}"></td>
-                  <td th:utext="${z.betragHtml}"></td>
-                  <td th:utext="${z.fristHtml}"></td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- Kinderbetreuungskosten -->
-    <div class="kinderbetreuung-container">
-      <div class="section-container">
-        <div class="section-header" onclick="toggleSection('kinderbetreuung')" role="button" tabindex="0" 
-             onkeypress="handleKeyPress(event, 'kinderbetreuung')" aria-expanded="true" aria-controls="kinderbetreuung-content">
-          <span class="toggle-icon" id="kinderbetreuung-icon" aria-hidden="true">▼</span>
-          <h3>Kinderbetreuungskosten</h3>
-        </div>
-        <div class="section-content" id="kinderbetreuung-content">
-          <table role="table" aria-labelledby="kinderbetreuung-caption" id="kinderbetreuung-table">
-            <thead>
-              <tr>
-                <th scope="col">Änderung</th>
-                <th scope="col">Kind</th>
-                <th scope="col">Betrag</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr th:each="z : ${kitaZeilen}" th:class="${z.rowClass}">
-                <td class="sym" th:classappend="' ' + ${z.symbolClass}" th:text="${z.symbol}"
-                    th:aria-label="${z.symbol == '+' ? 'Hinzugefügt' : (z.symbol == '-' ? 'Gelöscht' : (z.symbol == '!' ? 'Geändert' : 'Unverändert'))}"></td>
-                <td th:utext="${z.kindNameHtml}"></td>
-                <td th:utext="${z.betragHtml}"></td>
-              </tr>
-            </tbody>
-          </table>
-        </div>
-      </div>
-    </div>
-  </main>
-
-</body>
-</html>