Răsfoiți Sursa

Umbau auf Stammakte, testdaten hinzugefügt template angepasst.

Leonard Schulz 1 an în urmă
părinte
comite
994b95816c

+ 120 - 49
src/main/java/com/tsystems/inwo/vgl/web/VergleichController.java

@@ -54,7 +54,6 @@ public class VergleichController {
                 model.addAttribute("rightJson", rightJson);
                 model.addAttribute("validate", validate);
                 model.addAttribute("schemaName", schemaName);
-                // versuche dennoch Periodenlisten zu füllen (best-effort) optional
                 return "test";
             }
         }
@@ -69,6 +68,9 @@ public class VergleichController {
         JsonNode stammvorgangJsonNode = findStammvorgangByZeitraumId(right, rightZeitraumId);
         StammvorgangInfo stammvorgangRight = createStammvorgangInfo(stammvorgangJsonNode);
 
+        // --- Extract stammakte info if available ---
+        StammakteInfo stammakteRight = extractStammakteInfo(right);
+
         // --- Links (Baseline) Kandidaten & Auswahl ---
         List<Vorgang> leftCand = parseLeftCandidates(left);
         List<Z> leftZ = List.of();
@@ -108,9 +110,6 @@ public class VergleichController {
         List<FreibetragZeile> freibetragZeilen = buildFreibetragZeilen(rightPerson, leftPerson);
         List<KitaZeile> kitaZeilen = buildKitaZeilen(rightPerson, leftPerson, personNameByIdRight);
 
-        // Summary aus Status zählen (einfach aus allen Zeilen)
-        Summary summary = countSummary(mitgliederRows, einkommenZeilen, freibetragZeilen, kitaZeilen);
-
         // --- Model füllen & rendern ---
         model.addAttribute("leftJson", leftJson);
         model.addAttribute("rightJson", rightJson);
@@ -123,6 +122,7 @@ public class VergleichController {
         model.addAttribute("zeitraeumeRight", zeitrechte);
         model.addAttribute("rightZeitraumId", rightZeitraumId);
         model.addAttribute("stammvorgangRight", stammvorgangRight);
+        model.addAttribute("stammakteRight", stammakteRight);
         model.addAttribute("personenRight", personenRight);
         model.addAttribute("personId", personId);
         model.addAttribute("mitgliederRows", mitgliederRows);
@@ -130,7 +130,6 @@ public class VergleichController {
         model.addAttribute("einkommenZeilen", einkommenZeilen);
         model.addAttribute("freibetragZeilen", freibetragZeilen);
         model.addAttribute("kitaZeilen", kitaZeilen);
-        model.addAttribute("summary", summary);
         return "vergleich";
     }
 
@@ -139,6 +138,7 @@ public class VergleichController {
     public record Vorgang(String id, String aktenzeichen, List<Z> zeitraeume) {}
     public record PersonRef(String id, String label) {}
     public record StammvorgangInfo(String id, String aktenzeichen, String beginn, String ende, String bemerkung) {}
+    public record StammakteInfo(String aktenzeichen, String antragstellername, String antragstellerGeburtsdatum, String anschrift) {}
     public static final class Pair<R, L> {
         public final R right;
         public final L left;
@@ -163,11 +163,61 @@ public class VergleichController {
         return new StammvorgangInfo(id, aktenzeichen, beginn, ende, bemerkung);
     }
 
+    private static StammakteInfo extractStammakteInfo(JsonNode root) {
+        JsonNode stammakte = root.path("stammakte");
+        if (stammakte.isMissingNode()) {
+            return new StammakteInfo("", "", "", "");
+        }
+        
+        String aktenzeichen = stammakte.path("aktenzeichen").asText("");
+        String antragstellername = stammakte.path("antragstellername").asText("");
+        String antragstellerGeburtsdatum = stammakte.path("antragstellerGeburtsdatum").asText("");
+        
+        JsonNode anschrift = stammakte.path("anschrift");
+        String anschriftStr = "";
+        if (!anschrift.isMissingNode()) {
+            StringBuilder sb = new StringBuilder();
+            String strasse = anschrift.path("strasse").asText("");
+            String hausnummer = anschrift.path("hausnummer").asText("");
+            String zusatz = anschrift.path("zusatz").asText("");
+            String plz = anschrift.path("plz").asText("");
+            String ort = anschrift.path("ort").asText("");
+            
+            if (!strasse.isEmpty()) sb.append(strasse);
+            if (!hausnummer.isEmpty()) sb.append(" ").append(hausnummer);
+            if (!zusatz.isEmpty()) sb.append(" ").append(zusatz);
+            if (!plz.isEmpty() || !ort.isEmpty()) {
+                if (sb.length() > 0) sb.append(", ");
+                if (!plz.isEmpty()) sb.append(plz);
+                if (!ort.isEmpty()) sb.append(" ").append(ort);
+            }
+            anschriftStr = sb.toString();
+        }
+        
+        return new StammakteInfo(aktenzeichen, antragstellername, antragstellerGeburtsdatum, anschriftStr);
+    }
+
     private static List<Z> extractZeitraeume(JsonNode root) {
         List<Z> result = new ArrayList<>();
         
-        // Check if this is the new vorgaenge structure
-        if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
+        // Check if this is the new stammakte structure
+        if (root.has("stammakte")) {
+            JsonNode stammakte = root.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
+                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                            String id = zeitraum.path("id").asText("");
+                            String beginn = zeitraum.path("beginn").asText("");
+                            String ende = zeitraum.path("ende").asText("");
+                            result.add(new Z(id, beginn, ende));
+                        }
+                    }
+                }
+            }
+        }
+        // Check if this is the vorgaenge structure
+        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
             // For vorgaenge structure, collect zeitraeume from all vorgaenge
             for (JsonNode vorgang : root.get("vorgaenge")) {
                 if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
@@ -193,8 +243,23 @@ public class VergleichController {
     }
     
     private static JsonNode findZeitraumById(JsonNode root, String id) {
-        // Check if this is the new vorgaenge structure
-        if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
+        // Check if this is the new stammakte structure
+        if (root.has("stammakte")) {
+            JsonNode stammakte = root.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
+                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                            if (id.equals(zeitraum.path("id").asText(""))) {
+                                return zeitraum;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        // Check if this is the vorgaenge structure
+        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
             // Search in all vorgaenge
             for (JsonNode vorgang : root.get("vorgaenge")) {
                 if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
@@ -217,8 +282,23 @@ public class VergleichController {
     }
     
     private static JsonNode findStammvorgangByZeitraumId(JsonNode root, String zeitraumId) {
-        // Check if this is the new vorgaenge structure
-        if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
+        // Check if this is the new stammakte structure
+        if (root.has("stammakte")) {
+            JsonNode stammakte = root.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode vorgang : stammakte.get("vorgaenge")) {
+                    if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
+                        for (JsonNode zeitraum : vorgang.get("zeitraeume")) {
+                            if (zeitraumId.equals(zeitraum.path("id").asText(""))) {
+                                return vorgang.path("stammvorgang");
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        // Check if this is the vorgaenge structure
+        else if (root.has("vorgaenge") && root.get("vorgaenge").isArray()) {
             // Search in all vorgaenge for the one containing this zeitraum
             for (JsonNode vorgang : root.get("vorgaenge")) {
                 if (vorgang.has("zeitraeume") && vorgang.get("zeitraeume").isArray()) {
@@ -239,13 +319,28 @@ public class VergleichController {
     private static List<Vorgang> parseLeftCandidates(JsonNode left) {
         List<Vorgang> result = new ArrayList<>();
         
-        if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
+        // Check if this is the new stammakte structure
+        if (left.has("stammakte")) {
+            JsonNode stammakte = left.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode v : stammakte.get("vorgaenge")) {
+                    JsonNode stammvorgang = v.path("stammvorgang");
+                    String id = stammvorgang.path("id").asText("");
+                    String aktenzeichen = stammvorgang.path("aktenzeichen").asText("");
+                    List<Z> zeitraeume = extractZeitraeume(JsonNodeFactory.instance.objectNode().set("vorgaenge", 
+                        JsonNodeFactory.instance.arrayNode().add(v)));
+                    result.add(new Vorgang(id, aktenzeichen, zeitraeume));
+                }
+            }
+        }
+        else if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
             // a) wenn left.vorgaenge Array
             for (JsonNode v : left.get("vorgaenge")) {
                 JsonNode stammvorgang = v.path("stammvorgang");
                 String id = stammvorgang.path("id").asText("");
                 String aktenzeichen = stammvorgang.path("aktenzeichen").asText("");
-                List<Z> zeitraeume = extractZeitraeume(v);
+                List<Z> zeitraeume = extractZeitraeume(JsonNodeFactory.instance.objectNode().set("vorgaenge", 
+                    JsonNodeFactory.instance.arrayNode().add(v)));
                 result.add(new Vorgang(id, aktenzeichen, zeitraeume));
             }
         } else {
@@ -261,7 +356,18 @@ public class VergleichController {
     }
     
     private static JsonNode getRootOfVorgang(JsonNode left, String vorgangId) {
-        if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
+        // Check if this is the new stammakte structure
+        if (left.has("stammakte")) {
+            JsonNode stammakte = left.get("stammakte");
+            if (stammakte.has("vorgaenge") && stammakte.get("vorgaenge").isArray()) {
+                for (JsonNode v : stammakte.get("vorgaenge")) {
+                    if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
+                        return v;
+                    }
+                }
+            }
+        }
+        else if (left.has("vorgaenge") && left.get("vorgaenge").isArray()) {
             for (JsonNode v : left.get("vorgaenge")) {
                 if (vorgangId.equals(v.path("stammvorgang").path("id").asText(""))) {
                     return v;
@@ -762,41 +868,6 @@ public class VergleichController {
             return escape(rightVal);
         }
     }
-    
-    private static Summary countSummary(List<MitgliedRow> m, List<EinkommenZeile> e, List<FreibetragZeile> f, List<KitaZeile> k) {
-        int added = 0, removed = 0, changed = 0, typeMismatch = 0;
-        
-        // Count MitgliedRow changes
-        for (MitgliedRow row : m) {
-            if ("plus".equals(row.symbolClass())) added++;
-            else if ("minus".equals(row.symbolClass())) removed++;
-            else if ("warn".equals(row.symbolClass())) changed++;
-        }
-        
-        // Count EinkommenZeile changes
-        for (EinkommenZeile row : e) {
-            if ("plus".equals(row.symbolClass())) added++;
-            else if ("minus".equals(row.symbolClass())) removed++;
-            else if ("warn".equals(row.symbolClass())) changed++;
-        }
-        
-        // Count FreibetragZeile changes
-        for (FreibetragZeile row : f) {
-            if ("plus".equals(row.symbolClass())) added++;
-            else if ("minus".equals(row.symbolClass())) removed++;
-            else if ("warn".equals(row.symbolClass())) changed++;
-        }
-        
-        // Count KitaZeile changes
-        for (KitaZeile row : k) {
-            if ("plus".equals(row.symbolClass())) added++;
-            else if ("minus".equals(row.symbolClass())) removed++;
-            else if ("warn".equals(row.symbolClass())) changed++;
-        }
-        
-        int total = added + removed + changed + typeMismatch;
-        return new Summary(added, removed, changed, typeMismatch, total);
-    }
 
     // Hilfs-Bausteine für Status und Zellen:
     private static String symFor(String status) {

+ 36 - 1
src/main/resources/schemas/haushalt-einkommen.schema.json

@@ -2,9 +2,18 @@
   "$id": "https://example.com/haushalt-einkommen.schema.json",
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "title": "Vergleichsansicht – Seite Haushaltsmitglieder & Einkommen",
-  "description": "Seite mit Vorgängen → Zeiträume → Personen je Zeitraum inkl. Einkommen, Freibeträge, Kinderbetreuung.",
+  "description": "Seite mit Stammakte → Vorgänge → Zeiträume → Personen je Zeitraum inkl. Einkommen, Freibeträge, Kinderbetreuung.",
   "type": "object",
   "oneOf": [
+    {
+      "additionalProperties": false,
+      "properties": {
+        "stammakte": {
+          "$ref": "#/$defs/Stammakte"
+        }
+      },
+      "required": ["stammakte"]
+    },
     {
       "additionalProperties": false,
       "properties": {
@@ -34,6 +43,32 @@
     }
   ],
   "$defs": {
+    "Stammakte": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "aktenzeichen": {
+          "type": "string"
+        },
+        "antragstellername": {
+          "type": "string"
+        },
+        "antragstellerGeburtsdatum": {
+          "type": "string",
+          "format": "date"
+        },
+        "anschrift": {
+          "$ref": "#/$defs/Adresse"
+        },
+        "vorgaenge": {
+          "type": "array",
+          "items": {
+            "$ref": "#/$defs/Vorgang"
+          }
+        }
+      },
+      "required": ["aktenzeichen", "antragstellername", "vorgaenge"]
+    },
     "Vorgang": {
       "type": "object",
       "additionalProperties": false,

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

@@ -0,0 +1,105 @@
+{
+  "stammakte": {
+    "aktenzeichen": "SA-2026-001",
+    "antragstellername": "Müller, Tobias",
+    "antragstellerGeburtsdatum": "1990-01-04",
+    "anschrift": {
+      "strasse": "Musterstraße",
+      "hausnummer": "123",
+      "plz": "12345",
+      "ort": "Musterstadt"
+    },
+    "vorgaenge": [
+      {
+        "stammvorgang": {
+          "id": "SV-001",
+          "aktenzeichen": "01/01/119979",
+          "beginn": "2026-01-01",
+          "ende": "2027-06-30"
+        },
+        "zeitraeume": [
+          {
+            "id": "Z1",
+            "beginn": "2026-01-01",
+            "ende": "2026-06-30",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": {
+                  "id": "P1",
+                  "nachname": "Müller",
+                  "vorname": "Tobias",
+                  "geburtsdatum": "1990-01-04",
+                  "geschlecht": "Männlich",
+                  "familienstand": "verheiratet",
+                  "verwandtschaftsverhaeltnis": "Ehe-/Lebenspartner",
+                  "sozialstellung": "Angestellter"
+                },
+                "einkommen": [
+                  { "id": "E1", "art": "NS - Eink. aus nichtselbst. Tätigkeit", "frequenz": 12, "betrag": 800000, "zuschlag": 0, "abschlag": 0, "werbungskosten": 0 },
+                  { "id": "E3", "art": "Nebenjob (Minijob)", "frequenz": 12, "betrag": 100000 }
+                ],
+                "freibetraege": []
+              },
+              {
+                "rolleImHaushalt": "Partnerin",
+                "person": {
+                  "id": "P2",
+                  "nachname": "Müller",
+                  "vorname": "Annemarie",
+                  "geburtsdatum": "1991-08-08",
+                  "geschlecht": "Weiblich",
+                  "familienstand": "verheiratet",
+                  "verwandtschaftsverhaeltnis": "Ehe-/Lebenspartner",
+                  "sozialstellung": "Angestellte"
+                },
+                "einkommen": [
+                  { "id": "E2", "art": "NS - Eink. aus nichtselbst. Tätigkeit", "frequenz": 12, "betrag": 600000 }
+                ],
+                "freibetraege": [
+                  { "id": "F1", "art": "Erwerbstätigenfreibetrag", "frequenz": 12, "betrag": 10000 }
+                ]
+              }
+            ]
+          },
+          {
+            "id": "Z2",
+            "beginn": "2026-07-01",
+            "ende": "2026-12-31",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": { "id": "P1", "nachname": "Müller", "vorname": "Tobias" },
+                "einkommen": [
+                  { "id": "E1", "art": "NS - Eink. aus nichtselbst. Tätigkeit", "frequenz": 12, "betrag": 810000 }
+                ]
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "stammvorgang": {
+          "id": "SV-002",
+          "aktenzeichen": "02/02/202000",
+          "beginn": "2025-01-01",
+          "ende": "2025-12-31",
+          "bemerkung": "Altfall"
+        },
+        "zeitraeume": [
+          {
+            "id": "Z9",
+            "beginn": "2025-01-01",
+            "ende": "2025-06-30",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": { "id": "P9", "nachname": "Beispiel", "vorname": "Alice" }
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}

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

@@ -0,0 +1,84 @@
+{
+  "stammakte": {
+    "aktenzeichen": "SA-2026-001",
+    "antragstellername": "Müller, Tobias",
+    "antragstellerGeburtsdatum": "1990-01-04",
+    "anschrift": {
+      "strasse": "Musterstraße",
+      "hausnummer": "123",
+      "zusatz": "Wohnung 2A",
+      "plz": "12345",
+      "ort": "Musterstadt"
+    },
+    "vorgaenge": [
+      {
+        "stammvorgang": {
+          "id": "SV-001",
+          "aktenzeichen": "01/01/119979",
+          "beginn": "2026-01-01",
+          "ende": "2027-06-30",
+          "bemerkung": "Weiterleistungsantrag"
+        },
+        "zeitraeume": [
+          {
+            "id": "Z1",
+            "beginn": "2026-01-01",
+            "ende": "2026-06-30",
+            "personen": [
+              {
+                "rolleImHaushalt": "Antragsteller",
+                "person": {
+                  "id": "P1",
+                  "nachname": "Müller",
+                  "vorname": "Tobias",
+                  "geburtsdatum": "1990-01-04",
+                  "geschlecht": "Männlich",
+                  "familienstand": "verheiratet",
+                  "verwandtschaftsverhaeltnis": "Ehe-/Lebenspartner",
+                  "sozialstellung": "Angestellter"
+                },
+                "einkommen": [
+                  { "id": "E1", "art": "NS - Eink. aus nichtselbst. Tätigkeit", "frequenz": 12, "betrag": 1000000, "werbungskosten": 10000 }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Partnerin",
+                "person": {
+                  "id": "P2",
+                  "nachname": "Müller",
+                  "vorname": "Annemarie",
+                  "geburtsdatum": "1991-08-08",
+                  "geschlecht": "Weiblich",
+                  "familienstand": "verheiratet",
+                  "verwandtschaftsverhaeltnis": "Ehe-/Lebenspartner",
+                  "sozialstellung": "Angestellte"
+                },
+                "einkommen": [
+                  { "id": "E2", "art": "NS - Eink. aus nichtselbst. Tätigkeit", "frequenz": 12, "betrag": 620000, "werbungskosten": 10000 }
+                ],
+                "freibetraege": [
+                  { "id": "F1", "art": "Erwerbstätigenfreibetrag", "frequenz": 12, "betrag": 12000 }
+                ],
+                "kinderbetreuung": [
+                  { "id": "K1", "kindRef": "P3", "betrag": 10000 }
+                ]
+              },
+              {
+                "rolleImHaushalt": "Kind",
+                "person": {
+                  "id": "P3",
+                  "nachname": "Müller",
+                  "vorname": "Sofie",
+                  "geburtsdatum": "2025-01-16",
+                  "geschlecht": "Weiblich",
+                  "familienstand": "ledig",
+                  "istKind": true
+                }
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}

+ 168 - 1
src/main/resources/static/styles.css

@@ -2,6 +2,7 @@
   --rot-hell:#FFD6D6; --gelb-hell:#FFF5CC; --gruen-hell:#D6F5D6;
   --rot:#800000; --gelb:#784801; --gruen:#006633;
   --weiss:#FFFFFF; --schwarz:#000000; --grau-hell:#F7F9FA; --grau-stark:#D7D8D9;
+  --berlin-rot:#E20613; --diwo-blau:#0052A5;
 }
 *{box-sizing:border-box}
 body{margin:0;padding:16px;background:var(--grau-hell);color:var(--schwarz);font:14px/1.4 system-ui,sans-serif}
@@ -14,6 +15,159 @@ textarea,input,select{width:100%;padding:.5rem;background:var(--weiss);color:var
 button{padding:.5rem .9rem;border:1px solid var(--grau-stark);background:#fff;cursor:pointer}
 .error-box{background:var(--rot-hell);border-left:4px solid var(--rot);padding:.6rem;margin:.6rem 0}
 
+/* DIWO Header Styles */
+.diwo-header {
+  background: var(--weiss);
+  border: 1px solid var(--grau-stark);
+  padding: 12px 16px;
+  margin-bottom: 16px;
+}
+
+.header-row-1 {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 8px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid var(--grau-stark);
+}
+
+.berlin-logo {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  background: var(--berlin-rot);
+  color: var(--weiss);
+  padding: 4px 8px;
+  font-weight: bold;
+  font-size: 12px;
+}
+
+.berlin-text {
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.berlin-bear {
+  font-size: 14px;
+}
+
+.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;
+  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;
+  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;
+}
+
+.zeitraum-controls {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.zeitraum-form {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.zeitraum-form label {
+  font-size: 13px;
+  font-weight: bold;
+  white-space: nowrap;
+}
+
+.zeitraum-select {
+  width: 200px;
+  padding: 4px 8px;
+  font-size: 13px;
+}
+
+.wechseln-btn {
+  padding: 4px 8px;
+  font-size: 12px;
+  min-width: 30px;
+}
+
+/* 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;
+}
+
+.legende-spacer {
+  color: var(--grau-stark);
+  font-weight: bold;
+}
+
 table{width:100%;border-collapse:collapse;background:var(--weiss);border:1px solid var(--grau-stark);margin:.6rem 0 1rem}
 th,td{border:1px solid var(--grau-stark);padding:.5rem;vertical-align:top}
 th{background:var(--grau-stark);position:sticky;top:0}
@@ -27,6 +181,19 @@ th{background:var(--grau-stark);position:sticky;top:0}
 .old{display:block;font-size:.85em;margin-top:.2rem;opacity:.9}
 .header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
 .legend{margin:.5rem 0 1rem}
-.person-selected{outline:3px solid var(--grau-stark);outline-offset:-3px}
+
+/* Person Selection - Blue outline for selected person */
+.person-selected{outline:3px solid var(--diwo-blau);outline-offset:-3px}
+
+/* Clickable table rows */
+.person-row {
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+}
+
+.person-row:hover {
+  background-color: rgba(0, 82, 165, 0.1);
+}
+
 .table-action{padding:.25rem .5rem;border:1px solid var(--grau-stark);background:#fff;cursor:pointer;font-size:.85rem}
 .table-action:hover{filter:brightness(0.98)}

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

@@ -3,14 +3,20 @@
 <head><meta charset="UTF-8"/><title>Vergleich – Test</title><link rel="stylesheet" href="/styles.css"/></head>
 <body>
   <h1>Vergleich – Testseite</h1>
+  
+  <div class="row" style="margin-bottom: 1rem;">
+    <button type="button" onclick="loadExamples()">📄 Beispiel-JSONs laden</button>
+    <button type="button" onclick="clearTextareas()">🗑️ Leeren</button>
+  </div>
+  
   <div th:if="${schemaErrors != null}" class="error-box">
     <strong>Schema-Fehler</strong>
     <ul><li th:each="e : ${schemaErrors}" th:text="${e}"></li></ul>
   </div>
 
   <form method="post" action="/vergleich" class="grid-2">
-    <div><label>Left JSON (Basis/Alt)</label><textarea name="leftJson" th:text="${leftJson}"></textarea></div>
-    <div><label>Right JSON (Neu/Wahrheit)</label><textarea name="rightJson" th:text="${rightJson}"></textarea></div>
+    <div><label>Left JSON (Basis/Alt)</label><textarea id="leftJson" name="leftJson" th:text="${leftJson}" rows="20"></textarea></div>
+    <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" name="schemaName" th:value="${schemaName ?: 'haushalt-einkommen'}"></label>
@@ -51,4 +57,42 @@
       <button type="submit">Ansicht aktualisieren</button>
     </form>
   </div>
+
+  <script>
+    async function loadExamples() {
+      try {
+        // Lade Alt-Beispiel
+        const altResponse = await fetch('/example-alt.json');
+        const altJson = await altResponse.json();
+        
+        // Lade Neu-Beispiel
+        const neuResponse = await fetch('/example-neu.json');
+        const neuJson = await neuResponse.json();
+        
+        // Fülle Textareas mit formatierten JSONs
+        document.getElementById('leftJson').value = JSON.stringify(altJson, null, 2);
+        document.getElementById('rightJson').value = JSON.stringify(neuJson, null, 2);
+        
+        console.log('Beispiel-JSONs erfolgreich geladen');
+      } catch (error) {
+        console.error('Fehler beim Laden der Beispiel-JSONs:', error);
+        alert('Fehler beim Laden der Beispiel-JSONs: ' + error.message);
+      }
+    }
+    
+    function clearTextareas() {
+      document.getElementById('leftJson').value = '';
+      document.getElementById('rightJson').value = '';
+    }
+    
+    // Automatisches Laden beim Seitenaufruf, falls Textareas leer sind
+    window.addEventListener('load', function() {
+      const leftJson = document.getElementById('leftJson');
+      const rightJson = document.getElementById('rightJson');
+      
+      if (!leftJson.value.trim() && !rightJson.value.trim()) {
+        loadExamples();
+      }
+    });
+  </script>
 </body></html>

+ 105 - 50
src/main/resources/templates/vergleich.html

@@ -1,58 +1,98 @@
 <!DOCTYPE html>
 <html lang="de" xmlns:th="http://www.thymeleaf.org">
-<head><meta charset="UTF-8"/><title>Vergleich – Haushaltsmitglieder & Einkommen</title><link rel="stylesheet" href="/styles.css"/></head>
+<head><meta charset="UTF-8"/><title>DIWO Vergleich – Haushaltsmitglieder/Einkommen</title><link rel="stylesheet" href="/styles.css"/></head>
 <body>
-  <header class="header">
-    <div>
-      <h1>Vergleich – Haushaltsmitglieder & Einkommen</h1>
-      <div class="muted" th:if="${stammvorgangRight != null}">
-        Aktenzeichen: <span th:text="${stammvorgangRight.aktenzeichen}">—</span>
+  <header class="diwo-header">
+    <div class="header-row-1">
+      <div class="berlin-logo">
+        <span class="berlin-text">BERLIN</span>
+        <span class="berlin-bear">🐻</span>
+      </div>
+      <div class="diwo-title">
+        <h1>DIWO</h1>
+        <h2>Vergleich</h2>
+      </div>
+      <div class="page-title">Haushaltsmitglieder/Einkommen</div>
+    </div>
+    
+    <div class="header-row-2">
+      <div class="aktenzeichen" th:if="${stammakteRight != null and !#strings.isEmpty(stammakteRight.aktenzeichen)}">
+        <span th:text="${stammakteRight.aktenzeichen}">01/01/119979</span>
+      </div>
+      <div class="aktenzeichen" th:if="${(stammakteRight == null or #strings.isEmpty(stammakteRight.aktenzeichen)) and stammvorgangRight != null}">
+        <span th:text="${stammvorgangRight.aktenzeichen}">01/01/119979</span>
+      </div>
+      
+      <div class="antragsteller-info" th:if="${stammakteRight != null}">
+        <span th:if="${!#strings.isEmpty(stammakteRight.antragstellername)}" th:text="${stammakteRight.antragstellername}">Müller, Tobias</span>
+        <span th:if="${!#strings.isEmpty(stammakteRight.antragstellerGeburtsdatum)}"> - <span th:text="${stammakteRight.antragstellerGeburtsdatum}">01.01.2000</span></span>
+      </div>
+      
+      <div class="location-info" th:if="${stammakteRight != null and !#strings.isEmpty(stammakteRight.anschrift)}">
+        <span th:text="${stammakteRight.anschrift}">Unter den Linden 1, 10117 Berlin</span>
+      </div>
+    </div>
+    
+    <div class="header-row-3">
+      <div class="vorgang-info" th:if="${stammvorgangRight != null}">
+        <span>Vorgang in der Bearbeitung: </span>
+        <span th:if="${!#strings.isEmpty(stammvorgangRight.bemerkung)}" th:text="${stammvorgangRight.bemerkung}">Weiterleistungsantrag</span>
+        <span th:if="${#strings.isEmpty(stammvorgangRight.bemerkung)}">-</span>
+        <span th:if="${!#strings.isEmpty(stammvorgangRight.beginn) and !#strings.isEmpty(stammvorgangRight.ende)}">
+          (<span th:text="${stammvorgangRight.beginn}">01.01.2026</span> - <span th:text="${stammvorgangRight.ende}">31.12.2027</span>)
+        </span>
+        <span> - umzustellen</span>
+      </div>
+      
+      <div class="zeitraum-controls">
+        <form method="post" action="/vergleich" class="zeitraum-form">
+          <input type="hidden" name="schemaName" th:value="${schemaName}"/>
+          <input type="hidden" name="validate" th:value="${validate}"/>
+          <input type="hidden" name="leftJson" th:value="${leftJson}"/>
+          <input type="hidden" name="rightJson" th:value="${rightJson}"/>
+          <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
+          <input type="hidden" name="leftZeitraumId" th:value="${leftZeitraumId}"/>
+          
+          <label>Zeitraum:</label>
+          <select name="rightZeitraumId" class="zeitraum-select">
+            <option th:each="z : ${zeitraeumeRight}" th:value="${z.id}"
+                    th:selected="${z.id == rightZeitraumId}"
+                    th:text="${z.beginn} + ' - ' + ${z.ende}"></option>
+          </select>
+          <button type="submit" class="wechseln-btn">▼</button>
+        </form>
       </div>
     </div>
-    <form method="post" action="/vergleich" class="row">
-      <input type="hidden" name="schemaName" th:value="${schemaName}"/>
-      <input type="hidden" name="validate" th:value="${validate}"/>
-      <input type="hidden" name="leftJson" th:value="${leftJson}"/>
-      <input type="hidden" name="rightJson" th:value="${rightJson}"/>
-      <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
-      <input type="hidden" name="leftZeitraumId" th:value="${leftZeitraumId}"/>
-      <label>Zeitraum (rechts):
-        <select name="rightZeitraumId">
-          <option th:each="z : ${zeitraeumeRight}" th:value="${z.id}"
-                  th:selected="${z.id == rightZeitraumId}"
-                  th:text="${z.beginn} + ' – ' + ${z.ende}"></option>
-        </select>
-      </label>
-      <button type="submit">Wechseln</button>
-    </form>
   </header>
 
-  <section class="legend">
-    <span class="sym plus">+</span> hinzugefügt (grün) &nbsp; 
-    <span class="sym minus">−</span> gelöscht (rot) &nbsp; 
-    <span class="sym warn">!</span> geändert (gelb je Feld)
-  </section>
-
-  <section class="summary" th:if="${summary != null}">
-    <span>Neu: <b th:text="${summary.added}">0</b></span>
-    <span>Gelöscht: <b th:text="${summary.removed}">0</b></span>
-    <span>Geändert: <b th:text="${summary.changed}">0</b></span>
-    <span>Typkonflikte: <b th:text="${summary.typeMismatch}">0</b></span>
-    <span>Gesamt: <b th:text="${summary.total}">0</b></span>
+  <!-- Vergleichslegende -->
+  <section class="vergleich-legende">
+    <div class="legende-item">
+      <span class="sym plus">+</span> <span>hinzugefügt (grün)</span>
+    </div>
+    <div class="legende-spacer">|</div>
+    <div class="legende-item">
+      <span class="sym minus">−</span> <span>gelöscht (rot)</span>
+    </div>
+    <div class="legende-spacer">|</div>
+    <div class="legende-item">
+      <span class="sym warn">!</span> <span>geändert (gelb)</span>
+    </div>
   </section>
 
-  <!-- Haushaltsmitglieder (rechts Master). Auswahl NUR hier über „Wählen" -->
+  <!-- Haushaltsmitglieder - klickbare Zeilen -->
   <h2>Haushaltsmitglieder</h2>
   <table>
     <thead>
       <tr>
         <th>Δ</th><th>Name</th><th>Geburtsdatum</th><th>Familienstand</th>
-        <th>Geschlecht</th><th>Verwandtschaft</th><th>Sozialstellung</th><th>Kind</th><th>Aktion</th>
+        <th>Geschlecht</th><th>Verwandtschaft</th><th>Sozialstellung</th><th>Kind</th>
       </tr>
     </thead>
     <tbody>
       <tr th:each="r : ${mitgliederRows}"
-          th:class="${r.rowClass} + ((${r.personId} == ${personId}) ? ' person-selected' : '')">
+          th:class="'person-row ' + ${r.rowClass} + ((${r.personId} == ${personId}) ? ' person-selected' : '')"
+          th:data-person-id="${r.personId}">
         <td class="sym" th:classappend="' ' + ${r.symbolClass}" th:text="${r.symbol}"></td>
         <td th:utext="${r.nameHtml}"></td>
         <td th:utext="${r.geburtsdatumHtml}"></td>
@@ -61,23 +101,22 @@
         <td th:utext="${r.verwandtschaftHtml}"></td>
         <td th:utext="${r.sozialstellungHtml}"></td>
         <td th:utext="${r.istKindHtml}"></td>
-        <td>
-          <form method="post" action="/vergleich">
-            <input type="hidden" name="leftJson" th:value="${leftJson}"/>
-            <input type="hidden" name="rightJson" th:value="${rightJson}"/>
-            <input type="hidden" name="validate" th:value="${validate}"/>
-            <input type="hidden" name="schemaName" th:value="${schemaName}"/>
-            <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
-            <input type="hidden" name="leftZeitraumId" th:value="${leftZeitraumId}"/>
-            <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
-            <input type="hidden" name="personId" th:value="${r.personId}"/>
-            <button class="table-action" type="submit">Wählen</button>
-          </form>
-        </td>
       </tr>
     </tbody>
   </table>
 
+  <!-- Hidden form for person selection -->
+  <form id="personForm" method="post" action="/vergleich" style="display: none;">
+    <input type="hidden" name="leftJson" th:value="${leftJson}"/>
+    <input type="hidden" name="rightJson" th:value="${rightJson}"/>
+    <input type="hidden" name="validate" th:value="${validate}"/>
+    <input type="hidden" name="schemaName" th:value="${schemaName}"/>
+    <input type="hidden" name="leftVorgangId" th:value="${leftVorgangId}"/>
+    <input type="hidden" name="leftZeitraumId" th:value="${leftZeitraumId}"/>
+    <input type="hidden" name="rightZeitraumId" th:value="${rightZeitraumId}"/>
+    <input type="hidden" name="personId" id="selectedPersonId"/>
+  </form>
+
   <h2>Details zur Person</h2>
   <h3 th:text="${personLabel}">—</h3>
 
@@ -123,4 +162,20 @@
       </tr>
     </tbody>
   </table>
+
+  <script>
+    document.addEventListener('DOMContentLoaded', function() {
+      // Add click event listeners to all person rows
+      const personRows = document.querySelectorAll('.person-row');
+      personRows.forEach(function(row) {
+        row.addEventListener('click', function() {
+          const personId = this.dataset.personId;
+          if (personId) {
+            document.getElementById('selectedPersonId').value = personId;
+            document.getElementById('personForm').submit();
+          }
+        });
+      });
+    });
+  </script>
 </body></html>